使用 Go、Chi、Postgres 和 sqlx 的 REST API 第二部分

举报
Q神 发表于 2023/06/24 15:15:45 2023/06/24
【摘要】 这是之前发布的带有 Go、Chi 和 InMemory Store 的 REST API的延续。在本教程中,我将扩展该服务以将数据存储在Postgres数据库中。我将使用Docker来运行 Postgres 并运行数据库迁移。项目设置我将首先复制 的内容https://github.com/kashifsoofi/blog-code-samples/tree/main/movies-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和另一个名为migrationsdb 的文件夹。我执行了以下命令来创建迁移。

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脚本downup将在应用迁移时执行,并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
}

数据库配置

添加一个名为Databasein的新结构config.go并将其添加到Configurationstruct 中。

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存储库中。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。