使用 Go、Chi、Postgres 和 sqlx 的 REST API 第二部分
这是之前发布的带有 Go、Chi 和 InMemory Store 的 REST API的延续。在本教程中,我将扩展该服务以将数据存储在Postgres数据库中。我将使用Docker来运行 Postgres 并运行数据库迁移。
项目设置
我将首先复制 的内容https://github.com/kashifsoofi/blog-code-samples/tree/main/movies-api-with-go-chi-and-memory-store
,将其放入新文件夹中movies-api-with-go-chi-and-postgres
,并更新模块名称以go.mod
匹配新文件夹,并在使用其的源文件中进行更新。这通常是您的git
存储库的根目录,并且不会像这样详细。
设置数据库服务器
我将使用 docker-compose 在 docker 容器中运行 Postgres。这将允许我们添加更多我们的 REST API 依赖的服务,例如用于分布式缓存的 Redis 服务器。
让我们首先添加一个名为 的新文件docker-compose.dev-env.yml
,您可以随意命名它。添加以下内容以添加电影休息 API 的数据库实例。
version: '3.7'
services:
movies.db:
image: postgres:14-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=Password123
- POSTGRES_DB=moviesdb
volumes:
- moviesdbdata:/var/lib/postgresql/data/
ports:
- “5432:5432”
restart: on-failure
healthcheck:
test: [ “CMD-SHELL”, “pg_isready -q -d moviesdb -U Password123“]
timeout: 10s
interval: 5s
retries: 10
volumes:
moviesdbdata:
在 docker-compose 文件所在的解决方案的根目录打开一个终端,并执行以下命令来启动数据库服务器。
docker-compose -f docker-compose.dev-env.yml up -d
数据库迁移
在开始使用 Postgres 之前,我们需要创建一个表来存储数据。我将使用优秀的migrate数据库迁移工具,它也可以作为库导入。
对于迁移,我创建了一个文件夹db
和另一个名为migrations
db 的文件夹。我执行了以下命令来创建迁移。
migrate create -ext sql -dir db/migrations -seq extension_uuid_ossp_create
migrate create -ext sql -dir db/migrations -seq table_movies_create
这将创建 4 个文件,对于每个迁移都会有一个up
脚本down
,up
将在应用迁移时执行,并down
在回滚更改时执行。
000001_extension_uuid_ossp_create.up.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
000001_extension_uuid_ossp_create.down.sql
DROP EXTENSION IF EXISTS "uuid-ossp";
000002_table_movies_create.up.sql
CREATE TABLE IF NOT EXISTS movies (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
title VARCHAR(100) NOT NULL,
director VARCHAR(100) NOT NULL,
release_date TIMESTAMP NOT NULL,
ticket_price DECIMAL(12, 2) NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT (now() AT TIME ZONE 'utc') NOT NULL
)
000002_table_movies_create.down.sql
DROP TABLE IF EXISTS movies;
我通常创建一个包含所有数据库迁移和执行这些迁移的工具的容器。运行数据库迁移的Dockerfile如下
FROM migrate/migrate
# Copy all db files
COPY ./migrations /migrations
ENTRYPOINT [ "migrate", "-path", "/migrations", "-database"]
CMD ["postgresql://postgres:Password123@movies.db:5432/moviesdb?sslmode=disable up"]
在文件中添加以下内容docker-compose.dev-env.yml
以添加迁移容器并在启动时运行迁移。请记住,如果添加新的迁移,则需要删除容器和movies.db.migrations
映像才能在容器中添加新的迁移文件。
movies.db.migrations:
depends_on:
- movies.db
image: movies.db.migrations
build:
context: ./db/
dockerfile: Dockerfile
command: "postgresql://postgres:Password123@movies.db:5432/moviesdb?sslmode=disable up"
在 docker-compose 文件所在项目的根目录打开一个终端,然后执行以下命令来启动数据库服务器并应用迁移来创建uuid-ossp
扩展和movies
表。
docker-compose -f docker-compose.dev-env.yml up -d
Postgres 电影商店
我将使用sqlx来执行查询并将列映射到结构字段,反之亦然,sqlx
它是一个在 go 标准database/sql
库上提供一组扩展的库。
添加一个名为 的新文件postgres_movies_store.go
。添加一个PostgresMoviesStore
包含databaseUrl
指向 的新结构体和一个指向 的指针sqlx.DB
,还将帮助器方法添加connect
到数据库和close
连接。
package store
import (
"context"
"movies-api/store"
_ "github.com/jackc/pgx/stdlib"
"github.com/jmoiron/sqlx"
)
const driverName = "pgx"
type PostgresMoviesStore struct {
databaseUrl string
dbx *sqlx.DB
}
func NewPostgresMoviesStore(databaseUrl string) *PostgresMoviesStore {
return &PostgresMoviesStore{
databaseUrl: databaseUrl,
}
}
func (s *PostgresMoviesStore) connect(ctx context.Context) error {
dbx, err := sqlx.ConnectContext(ctx, driverName, s.databaseUrl)
if err != nil {
return err
}
s.dbx = dbx
return nil
}
func (s *PostgresMoviesStore) close() error {
return s.dbx.Close()
}
添加数据库标签
更新文件Movie
中的结构movies_store.go
以添加 db 标签,这允许 sqlx 将结构成员映射到列名称。
type Movie struct {
ID uuid.UUID
Title string
Director string
ReleaseDate time.Time `db:"release_date"`
TicketPrice float64 `db:"ticket_price"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
语境
Context
我们没有在早期示例中使用movies-api-with-go-chi-and-memory-store
,现在我们正在连接到外部存储和包,我们将使用它来运行查询支持方法,Context
我们将更新我们store.Interface
以在运行查询时接受Context
和使用它。store.Interface
将更新如下
type Interface interface {
GetAll(ctx context.Context) ([]Movie, error)
GetByID(ctx context.Context, id uuid.UUID) (Movie, error)
Create(ctx context.Context, createMovieParams CreateMovieParams) error
Update(ctx context.Context, id uuid.UUID, updateMovieParams UpdateMovieParams) error
Delete(ctx context.Context, id uuid.UUID) error
}
我们还需要更新MemoryMoviesStore
方法以接受Context
以满足store.Interface
更改,并更新方法以在调用方法时movies_handler
传递请求上下文。r.Context()
store
创造
我们使用connect
辅助方法连接到数据库,创建一个新实例Movie
并使用 执行插入查询NamedExecContext
。我们正在处理error
并返回DuplicateKeyError
如果SqlState
异常是23505
。如果插入成功则返回nil
。
创建函数看起来像
func (s *PostgresMoviesStore) Create(ctx context.Context, createMovieParams CreateMovieParams) error {
err := s.connect(ctx)
if err != nil {
return err
}
defer s.close()
movie := Movie{
ID: createMovieParams.ID,
Title: createMovieParams.Title,
Director: createMovieParams.Director,
ReleaseDate: createMovieParams.ReleaseDate,
TicketPrice: createMovieParams.TicketPrice,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if _, err := s.dbx.NamedExecContext(
ctx,
`INSERT INTO movies
(id, title, director, release_date, ticket_price, created_at, updated_at)
VALUES
(:id, :title, :director, :release_date, :ticket_price, :created_at, :updated_at)`,
movie); err != nil {
if strings.Contains(err.Error(), "SQLSTATE 23505") {
return &DuplicateKeyError{ID: createMovieParams.ID}
}
return err
}
return nil
}
得到所有
我们使用connect
辅助方法连接到数据库,然后使用SelectContext
方法sqlx
执行查询,sqlx
将列映射到字段。如果查询成功,那么我们返回加载的电影片段。
func (s *PostgresMoviesStore) GetAll(ctx context.Context) ([]Movie, error) {
err := s.connect(ctx)
if err != nil {
return nil, err
}
defer s.close()
var movies []Movie
if err := s.dbx.SelectContext(
ctx,
&movies,
`SELECT
id, title, director, release_date, ticket_price, created_at, updated_at
FROM movies`); err != nil {
return nil, err
}
return movies, nil
}
按ID获取
我们使用connect
辅助方法连接到数据库,然后使用GetContext
方法执行选择查询,sqlx
将列映射到字段。如果司机回来了,sql.ErrNoRows
我们就回来store.RecordNotFoundError
。如果加载成功movie
则返回记录。
func (s *PostgresMoviesStore) GetByID(ctx context.Context, id uuid.UUID) (Movie, error) {
err := s.connect(ctx)
if err != nil {
return Movie{}, err
}
defer s.close()
var movie Movie
if err := s.dbx.GetContext(
ctx,
&movie,
`SELECT
id, title, director, release_date, ticket_price, created_at, updated_at
FROM movies
WHERE id = $1`,
id); err != nil {
if err != sql.ErrNoRows {
return Movie{}, err
}
return Movie{}, &RecordNotFoundError{}
}
return movie, nil
}
更新
我们使用connect
辅助方法连接到数据库,然后使用NamedExecContext
方法执行查询来更新现有记录。
func (s *PostgresMoviesStore) Update(ctx context.Context, id uuid.UUID, updateMovieParams UpdateMovieParams) error {
err := s.connect(ctx)
if err != nil {
return err
}
defer s.close()
movie := Movie{
ID: id,
Title: updateMovieParams.Title,
Director: updateMovieParams.Director,
ReleaseDate: updateMovieParams.ReleaseDate,
TicketPrice: updateMovieParams.TicketPrice,
UpdatedAt: time.Now().UTC(),
}
if _, err := s.dbx.NamedExecContext(
ctx,
`UPDATE movies
SET title = :title, director = :director, release_date = :release_date, ticket_price = :ticket_price, updated_at = :updated_at
WHERE id = :id`,
movie); err != nil {
return err
}
return nil
}
删除
我们使用connect
辅助方法连接到数据库,然后使用 执行查询来删除现有记录ExecContext
。
func (s *PostgresMoviesStore) Delete(ctx context.Context, id uuid.UUID) error {
err := s.connect(ctx)
if err != nil {
return err
}
defer s.close()
if _, err := s.dbx.ExecContext(
ctx,
`DELETE FROM movies
WHERE id = $1`, id); err != nil {
return err
}
return nil
}
数据库配置
添加一个名为Database
in的新结构config.go
并将其添加到Configuration
struct 中。
type Configuration struct {
HTTPServer
Database
}
...
type Database struct {
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
LogLevel string `envconfig:"DATABASE_LOG_LEVEL" default:"warn"`
MaxOpenConnections int `envconfig:"DATABASE_MAX_OPEN_CONNECTIONS" default:"10"`
}
依赖注入
更新main.go
如下以创建 的新实例PostgresMoviesStore
,我选择创建 的实例而PostgresMoviesStore
不是MemoryMoviesStore
,可以增强解决方案以基于配置创建任一依赖项。
// store := store.NewMemoryMoviesStore()
store := store.NewPostgresMoviesStore(cfg.DatabaseURL)
测试
我不会为本教程(也许是后续教程)添加任何单元或集成测试。但是所有端点都可以使用 Postman 进行测试,按照上一篇文章中的测试计划进行测试。
您可以通过执行以下命令使用 postgres 启动rest api
DATABASE_URL=postgresql://postgres:Password123@localhost:5432/moviesdb?sslmode=disable go run main.go
来源
演示应用程序的源代码托管在 GitHub 上的blog-code-samples存储库中。
- 点赞
- 收藏
- 关注作者
评论(0)