与GO,Chi和MongoDB进行休息API
#api #go #休息 #mongodb

这是较早的REST API with Go, Chi and InMemory Store的延续。在本教程中,我将将服务扩展到将数据存储在MongoDB中,我将使用MongoDB Community Server Docker Image用于此示例。我将使用Docker运行mongodb。

设置数据库服务器

我将使用Docker-Compose在Docker容器中运行MongoDB。这将使我们添加更多的服务,例如,我们的REST API在例如REDIS服务器用于分布式缓存。

让我们从右键单击Visual Studio中的解决方案名称并添加新文件开始添加新文件。我喜欢将文件命名为docker-compose.dev-env.yml,请随时按照您的要求命名。添加以下内容以添加电影REST API的数据库实例。

version: '3.7'

services:
  movies.db:
    image: mongodb/mongodb-community-server:6.0.5-ubuntu2204
    environment:
      - MONGODB_INITDB_ROOT_USERNAME=root
      - MONGODB_INITDB_ROOT_PASSWORD=Password123
      - MONGO_INITDB_DATABASE=Movies
    volumes:
      - moviesdbdata:/data/db
    ports:
      - "27017:27017"

volumes:
  moviesdbdata:

在docker-compose文件为位置并执行以下命令以启动数据库服务器的解决方案的根部打开一个终端。

docker-compose -f docker-compose.dev-env.yml up -d

数据库迁移

我们不会对MongoDB进行任何数据库/模式迁移,因为它的NOSQL数据库,here是关于stackoverflow在此主题上的一个很好的讨论。我们不需要此示例的任何迁移,但是,如果需要出现,并且没有架构迁移脚本的强用例,我希望选择支持多个模式相互矛盾的途径并在需要时进行更新。

MongoDB电影商店

store文件夹下添加一个名为mongo_movies_store.go的新文件。添加一个包含databaseUrl的新结构MongoMoviesStore,一个指向mongo.Client的指针以及我们将使用的mongo.Collection指针。我们还将将助手方法添加到connect中的数据库和初始化的collection字段,我们将在每种CRUD方法中使用,并将助手方法用于close Connection。

package store

import (
    "context"
    "time"

    "github.com/google/uuid"
    "github.com/kashifsoofi/blog-code-samples/movies-api-with-go-chi-and-mongodb/config"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type MongoMoviesStore struct {
    database   config.Database
    client     *mongo.Client
    collection *mongo.Collection
}

func NewMongoMoviesStore(config config.Database) *MongoMoviesStore {
    return &MongoMoviesStore{
        database: config,
    }
}

func (s *MongoMoviesStore) connect(ctx context.Context) error {
    serverAPI := options.ServerAPI(options.ServerAPIVersion1)

    client, err := mongo.Connect(
        ctx,
        options.Client().ApplyURI(s.database.DatabaseURL).SetServerAPIOptions(serverAPI),
    )
    if err != nil {
        return err
    }

    s.client = client
    s.collection = s.client.Database(s.database.DatabaseName).Collection(s.database.MoviesCollectionName)
    return nil
}

func (s *MongoMoviesStore) close(ctx context.Context) error {
    return s.client.Disconnect(ctx)
}

添加DB标签

movies_store.go文件中更新Movie struct,以将标记添加到标记ID字段作为mongo的objectid。所有其他字段将像保存和加载文档时一样映射。

type Movie struct {
    ID          uuid.UUID `bson:"_id"`
    ...
}

语境

我们没有在较早的movies-api-with-go-chi-and-memory-store中使用Context,因为我们正在连接到一个外部存储和软件包,我们将用于运行查询方法接受Context我们将更新我们的store.Interface以接受Context并使用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 Helper方法连接到数据库,创建Movie的新实例,并使用新创建的实例执行InsertOne方法。如果返回错误是mongo DuplicateKeyError,我们正在处理error并返回DuplicateKeyError。如果插入成功,我们返回nil
创建功能看起来像

func (s *MongoMoviesStore) Create(ctx context.Context, createMovieParams CreateMovieParams) error {
    err := s.connect(ctx)
    if err != nil {
        return err
    }
    defer s.close(ctx)

    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.collection.InsertOne(ctx, movie); err != nil {
        if mongo.IsDuplicateKeyError(err) {
            return &DuplicateKeyError{ID: createMovieParams.ID}
        }
        return err
    }

    return nil
}

得到所有

我们使用connect Helper方法连接到数据库,我们在集合上调用Find方法,以获取带有空过滤器的cursor,以获取所有电影文档。然后,我们在光标上使用All方法将所有文档检索到一个切片中。

func (s *MongoMoviesStore) GetAll(ctx context.Context) ([]Movie, error) {
    err := s.connect(ctx)
    if err != nil {
        return nil, err
    }
    defer s.close(ctx)

    cur, err := s.collection.Find(ctx, bson.D{})
    if err != nil {
        return nil, err
    }
    defer cur.Close(ctx)

    var movies []Movie
    if err := cur.All(ctx, &movies); err != nil {
        return nil, err
    }

    return movies, nil
}

GetByid

我们使用connect Helper方法连接到数据库,我们通过传递请求的ID并将结果解码为Movie实例,调用集合上的FindOne方法。我们正在检查FindOne是否重新调整了ErrNoDocuments并将我们的自定义RecordNotFound错误返回给呼叫者。如果返回了从集合中加载的错误文档。

func (s *MongoMoviesStore) GetByID(ctx context.Context, id uuid.UUID) (Movie, error) {
    err := s.connect(ctx)
    if err != nil {
        return Movie{}, err
    }
    defer s.close(ctx)

    var movie Movie
    if err := s.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&movie); err != nil {
        if err == mongo.ErrNoDocuments {
            return Movie{}, &RecordNotFoundError{}
        }
        return Movie{}, err
    }

    return movie, nil
}

更新

我们使用connect Helper方法连接到数据库,然后使用updateMovieParams的字段进行准备和update设置,然后在我们的集合上调用UpdateOne方法以更新所有字段。

在这里,我们正在更新所有传递的字段,而不支持部分更新,这意味着Caller如果不想更改特定字段,则负责将字段正确设置为上一个值。可以增强此方法以支持部分更新。

func (s *MongoMoviesStore) Update(ctx context.Context, id uuid.UUID, updateMovieParams UpdateMovieParams) error {
    err := s.connect(ctx)
    if err != nil {
        return err
    }
    defer s.close(ctx)

    update := bson.M{
        "$set": bson.M{
            "Title":       updateMovieParams.Title,
            "Director":    updateMovieParams.Director,
            "ReleaseDate": updateMovieParams.ReleaseDate,
            "TicketPrice": updateMovieParams.TicketPrice,
            "UpdatedAt":   time.Now().UTC(),
        },
    }
    if _, err := s.collection.UpdateOne(ctx, bson.M{"_id": id}, update); err != nil {
        return err
    }

    return nil
}

删除

我们使用connect Helper方法连接到数据库,然后通过传递请求的ID删除记录,我们在集合上调用DeleteOne方法。

func (s *MongoMoviesStore) Delete(ctx context.Context, id uuid.UUID) error {
    err := s.connect(ctx)
    if err != nil {
        return err
    }
    defer s.close(ctx)

    if _, err := s.collection.DeleteOne(ctx, bson.M{"_id": id}); err != nil {
        return err
    }

    return nil
}

数据库配置

config.go中添加一个名为Database的新结构,也将其添加到Configuration结构中。

type Configuration struct {
    HTTPServer
    Database
}
...
type Database struct {
    DatabaseURL          string `envconfig:"DATABASE_URL" required:"true"`
    DatabaseName         string `envconfig:"DATABASE_NAME" default:"MoviesStore"`
    MoviesCollectionName string `envconfig:"MOVIES_COLLECTION_NAME" default:"MoviesCollectionName"`
}

依赖注入

更新main.go如下所示,以创建MongoMoviesStore的新实例,我选择创建MongoMoviesStore的实例,而不是MemoryMoviesStore,可以增强解决方案,以基于配置创建一个依赖性。


// store := store.NewMemoryMoviesStore()
store := store.NewMongoMoviesStore(cfg.Database)

测试

我没有为本教程添加任何单元或集成测试,也许是以下教程。但是所有端点都可以使用邮递员进行测试,以通过以下来自previous article的测试计划。

您可以通过执行以下
在Docker中运行SQL Server启动REST API

DATABASE_URL=mongodb://root:Password123@localhost:27017 go run main.go

来源

演示应用程序的源代码托管在blog-code-samples存储库的GitHub上。

参考

没有特定顺序