这是较早的REST API with Go, Chi and InMemory Store的延续。在本教程中,我将扩展服务以将数据存储在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
repo的根源,并且不会像这样详细说明。
设置数据库服务器
我将使用Docker-Compose在Docker容器中运行MySQL。这将使我们添加更多的服务,例如,我们的REST API在例如REDIS服务器用于分布式缓存。
让我们从添加一个名为docker-compose.dev-env.yml
的新文件开始,随时随意命名它。添加以下内容以添加电影REST 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
的文件夹。我执行了以下命令创建迁移的命令。
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;
i通常会创建一个具有所有数据库迁移和执行这些迁移的工具的容器。运行数据库迁移的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执行查询和映射列到struct字段,反之亦然,sqlx
是一个库,它在GO的标准database/sql
库中提供了一组扩展名。
添加一个名为mysql_movies_store.go
的新文件。添加一个包含databaseUrl
的新结构MySqlMoviesStore
和一个指向sqlx.DB
的指针,还将助手方法添加到connect
中,并将其添加到数据库和close
连接中。另请注意,我添加了noOpMapper
方法并将其设置为sqlx.DB
的mapperfunc,原因是使用与结构字段名称相同的壳体。 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()
}
添加DB标签
更新movies_store.go
文件中的Movie
struct以添加ID
字段的DB标签,这允许SQLX映射ID
字段以纠正列。替代方案是将AS
在数据库中的“ ID
”中使用AS
或重命名列名。所有其他字段将通过上面的noOpMapper
正确映射。
type Movie struct {
ID uuid.UUID `db:"Id"`
...
}
语境
我们没有在较早的movies-api-with-go-chi-and-memory-store
中使用Context
,因为我们正在连接到一个外部存储和软件包,我们将用于运行查询方法接受Context
我们将更新我们的store.Interface
以接受Context
并使用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
中更新方法,以在调用store
方法时使用r.Context()
传递请求上下文。
创造
我们使用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
参数添加到您的连接字符串。
GetByid
我们使用connect
助手方法连接到数据库,然后使用GetContext
方法执行SELECT QUERY,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
}
数据库配置
在config.go
中添加一个名为Database
的新结构
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)
测试
我没有为本教程添加任何单元或集成测试,也许是以下教程。但是所有终点都可以使用邮递员进行测试,以通过以下来自previous article的测试计划进行测试。
您可以通过执行以下
在Docker中运行MySQL启动REST API
DATABASE_URL=root:Password123@tcp(localhost:3306)/moviesdb?parseTime=true go run main.go
来源
演示应用程序的源代码托管在blog-code-samples存储库的GitHub上。
参考
没有特定顺序