使用 Go、Chi、MySQL 和 sqlx 的 REST API 第三部分
这是之前发布的带有 Go、Chi 和 InMemory Store 的 REST API的延续。在本教程中,我将扩展服务以将数据存储在MySQL数据库中。我将使用Docker来运行 MySQL 并运行数据库迁移。
项目设置
我将首先复制 的内容https://github.com/kashifsoofi/blog-code-samples/tree/main/movies-api-with-go-chi-and-memory-store
,将其放入新文件夹中movies-api-with-go-chi-and-mysql
,并更新模块名称以go.mod
匹配新文件夹,并在使用其的源文件中进行更新。这通常是您的git
存储库的根目录,并且不会像这样详细。
设置数据库服务器
我将使用 docker-compose 在 docker 容器中运行 MySQL。这将允许我们添加更多我们的 REST API 依赖的服务,例如用于分布式缓存的 Redis 服务器。
让我们首先添加一个名为 的新文件docker-compose.dev-env.yml
,您可以随意命名它。添加以下内容以添加电影休息 API 的数据库实例。
version: '3.7'
services:
movies.db:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=Password123
- MYSQL_DATABASE=moviesdb
volumes:
- moviesdbdata:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: "mysql -uroot -pPassword123 moviesdb -e 'select 1'"
timeout: 20s
interval: 10s
retries: 10
volumes:
moviesdbdata:
在 docker-compose 文件所在的解决方案的根目录打开一个终端,然后执行以下命令来启动数据库服务器。
docker-compose -f docker-compose.dev-env.yml up -d
数据库迁移
在开始使用 MySQL 之前,我们需要创建一个表来存储数据。我将使用优秀的migrate数据库迁移工具,它也可以作为库导入。
对于迁移,我创建了一个文件夹db
和另一个名为migrations
db 的文件夹。我执行了以下命令来创建迁移。
migrate create -ext sql -dir db/migrations -seq schema_movies_create
migrate create -ext sql -dir db/migrations -seq table_movies_create
这将创建 4 个文件,对于每个迁移都会有一个up
脚本down
,up
将在应用迁移时执行,并down
在回滚更改时执行。
000001_schema_movies_create.up.sql
CREATE SCHEMA IF NOT EXISTS Movies;
000001_schema_movies_create.down.sql
DROP SCHEMA IF EXISTS Movies;
000002_table_movies_create.up.sql
CREATE TABLE IF NOT EXISTS Movies (
Id CHAR(36) NOT NULL UNIQUE,
Title VARCHAR(100) NOT NULL,
Director VARCHAR(100) NOT NULL,
ReleaseDate DATETIME NOT NULL,
TicketPrice DECIMAL(12, 4) NOT NULL,
CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL,
PRIMARY KEY (Id)
) ENGINE=INNODB;
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 ["mysql://root:Password123@tcp(movies.db:3306)/moviesdb up"]
在文件中添加以下内容docker-compose.dev-env.yml
以添加迁移容器并在启动时运行迁移。请记住,如果添加新的迁移,则需要删除容器和movies.db.migrations
映像才能在映像中添加新的迁移文件。
movies.db.migrations:
depends_on:
movies.db:
condition: service_healthy
image: movies.db.migrations
build:
context: ./db/
dockerfile: Dockerfile
command: "'mysql://root:Password123@tcp(movies.db:3306)/moviesdb' up"
在 docker-compose 文件所在的项目根目录打开一个终端,并执行以下命令来启动数据库服务器并应用迁移来创建Movies
架构和Movies
表。
docker-compose -f docker-compose.dev-env.yml up -d
MySQL 电影商店
我将使用sqlx来执行查询并将列映射到结构字段,反之亦然,sqlx
它是一个在 go 标准database/sql
库上提供一组扩展的库。
添加一个名为 的新文件mysql_movies_store.go
。添加一个MySqlMoviesStore
包含databaseUrl
指向 的新结构体和一个指向 的指针sqlx.DB
,还将帮助器方法添加connect
到数据库和close
连接。另请注意,我添加了一个noOpMapper
方法并设置为 MapperFunc of sqlx.DB
,原因是使用与结构字段名称相同的大小写。默认行为sqlx
是将字段名称映射到小写列名称。
package store
import (
"context"
"database/sql"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
const driverName = "mysql"
type MySqlMoviesStore struct {
databaseUrl string
dbx *sqlx.DB
}
func NewMySqlMoviesStore(databaseUrl string) *MySqlMoviesStore {
return &MySqlMoviesStore{
databaseUrl: databaseUrl,
}
}
func noOpMapper(s string) string { return s }
func (s *MySqlMoviesStore) connect(ctx context.Context) error {
dbx, err := sqlx.ConnectContext(ctx, driverName, s.databaseUrl)
if err != nil {
return err
}
dbx.MapperFunc(noOpMapper)
s.dbx = dbx
return nil
}
func (s *MySqlMoviesStore) close() error {
return s.dbx.Close()
}
添加数据库标签
更新文件Movie
中的结构movies_store.go
以添加ID
字段的 db 标记,这允许 sqlx 将ID
字段映射到正确的列。替代方法是使用AS
in select 查询或将数据库中的列名称重命名为ID
. noOpMapper
所有其他字段都将使用上一节中的内容正确映射。
type Movie struct {
ID uuid.UUID `db:"Id"`
...
}
语境
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
Error 1062
nil
func (s *MySqlMoviesStore) 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, ReleaseDate, TicketPrice, CreatedAt, UpdatedAt)
VALUES
(:Id, :Title, :Director, :ReleaseDate, :TicketPrice, :CreatedAt, :UpdatedAt)`,
movie); err != nil {
if strings.Contains(err.Error(), "Error 1062") {
return &DuplicateKeyError{ID: createMovieParams.ID}
}
return err
}
return nil
}
得到所有
我们使用connect
辅助方法连接到数据库,然后使用SelectContext
方法sqlx
执行查询,sqlx
将列映射到字段。如果查询成功,那么我们返回加载的电影片段。
func (s *MySqlMoviesStore) 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, ReleaseDate, TicketPrice, CreatedAt, UpdatedAt
FROM Movies`); err != nil {
return nil, err
}
return movies, nil
}
如果解析列时出现错误DATETIME
,请记住将参数添加parseTime=true
到连接字符串中。
按ID获取
我们使用connect
辅助方法连接到数据库,然后使用GetContext
方法执行选择查询,sqlx
将列映射到字段。如果司机回来了,sql.ErrNoRows
我们就回来store.RecordNotFoundError
。如果加载成功movie
则返回记录。
func (s *MySqlMoviesStore) 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, ReleaseDate, TicketPrice, CreatedAt, UpdatedAt
FROM Movies
WHERE Id = ?`,
id); err != nil {
if err != sql.ErrNoRows {
return Movie{}, err
}
return Movie{}, &RecordNotFoundError{}
}
return movie, nil
}
更新
我们使用connect
辅助方法连接到数据库,然后使用NamedExecContext
方法执行查询来更新现有记录。
func (s *MySqlMoviesStore) 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, ReleaseDate = :ReleaseDate, TicketPrice = :TicketPrice, UpdatedAt = :UpdatedAt
WHERE Id = :Id`,
movie); err != nil {
return err
}
return nil
}
删除
我们使用connect
辅助方法连接到数据库,然后使用 执行查询来删除现有记录ExecContext
。
func (s *MySqlMoviesStore) 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 = ?`, 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
如下以创建 的新实例MySqlMoviesStore
,我选择创建 的实例而MySqlMoviesStore
不是MemoryMoviesStore
,可以增强解决方案以基于配置创建任一依赖项。
// store := store.NewMemoryMoviesStore()
store := store.NewMySqlMoviesStore(cfg.DatabaseURL)
测试
我不会为本教程(也许是后续教程)添加任何单元或集成测试。但是所有端点都可以使用 Postman 进行测试,按照上一篇文章中的测试计划进行测试。
您可以通过执行以下命令来启动rest api,并在docker中运行mysql
DATABASE_URL=root:Password123@tcp(localhost:3306)/moviesdb?parseTime=true go run main.go
来源
演示应用程序的源代码托管在 GitHub 上的blog-code-samples存储库中。
- 点赞
- 收藏
- 关注作者
评论(0)