在Golang中一个简单的MongoDB CRUD例子

134 阅读2分钟

这是用Golang编写的一个简单的MongoDB CRUD例子。它也有一个最基本的分页功能。请记住,有些文件需要改进。有一些硬编码的部分,重复的部分等等。我必须让这个帖子尽可能的简短。请随意重构它。

结构

├── cmd
│   └── blog
│       └── main.go
├── docker
│   ├── docker-compose.yaml
│   └── mongodb
│       └── init.sh
└── internal
    ├── comment
    │   ├── controller.go
    │   ├── request.go
    │   └── response.go
    └── pkg
        ├── domain
        │   └── error.go
        └── storage
            ├── comment_storer.go
            └── mongodb
                ├── comment.go
                └── database.go

文件

main.go

package main

import (
	"context"
	"log"
	"net/http"
	"time"

	"github.com/you/mongo/internal/comment"
	"github.com/you/mongo/internal/pkg/storage/mongodb"
	"github.com/julienschmidt/httprouter"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Initiate MongoDB instance.
	mng, err := mongodb.NewDatabase(ctx, mongodb.DatabaseConfig{
		Auth: "SCRAM-SHA-256",
		Host: "localhost",
		Port: "27017",
		User: "inanzzz",
		Pass: "1234567",
		Name: "blog",
	})
	if err != nil {
		log.Fatalln(err)
	}
	defer mng.Client().Disconnect(ctx)

	// Initiate storage.
	comStrg := mongodb.CommentStorage{
		Database: mng,
		Timeout:  time.Second * 5,
	}

	// Initiate API.
	com := comment.Controller{
		Storage: comStrg,
	}

	// Initiate HTTP router.
	rtr := httprouter.New()
	rtr.HandlerFunc("POST", "/api/v1/comments", com.Create)
	rtr.HandlerFunc("GET", "/api/v1/comments/:uuid", com.Find)
	rtr.HandlerFunc("DELETE", "/api/v1/comments/:uuid", com.Delete)
	rtr.HandlerFunc("PATCH", "/api/v1/comments/:uuid", com.Update)
	rtr.HandlerFunc("GET", "/api/v1/comments", com.List)

	// Initiate HTTP server.
	log.Fatalln((&http.Server{Addr: ":3000", Handler: rtr}).ListenAndServe())
}

docker-compose.yaml

version: "3.4"

services:

  blog-mongodb:
    container_name: "blog-mongodb"
    image: "mongo:4.0"
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: "root"
      MONGO_INITDB_ROOT_PASSWORD: "root"
      MONGO_NAME: "blog"
      MONGO_USER: "inanzzz"
      MONGO_PASS: "1234567"
      MONGO_AUTH: "SCRAM-SHA-256"
    volumes:
      - "./mongodb:/docker-entrypoint-initdb.d"

init.sh

#!/bin/sh

# Create a custom user with a password, role and auth mechanism. This user will
# be used by the application for MongoDB connection.
mongo \
    -u ${MONGO_INITDB_ROOT_USERNAME} \
    -p ${MONGO_INITDB_ROOT_PASSWORD} \
    --authenticationDatabase admin ${MONGO_NAME} \
<<-EOJS
db.createUser({
    user: "${MONGO_USER}",
    pwd: "${MONGO_PASS}",
    roles: [{
        role: "readWrite",
        db: "${MONGO_NAME}"
    }],
    mechanisms: ["${MONGO_AUTH}"],
})
EOJS

# Prepare database.
mongo \
    -u ${MONGO_INITDB_ROOT_USERNAME} \
    -p ${MONGO_INITDB_ROOT_PASSWORD} \
    --authenticationDatabase admin ${MONGO_NAME} \
<<-EOJS
use ${MONGO_NAME}
db.createCollection("comments")
db.comments.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})
EOJS

控制器.go

正如你所看到的,这个文件做了很多事情,而且没有一个真正的请求验证。你应该根据你的需要重构它。

package comment

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/you/mongo/internal/pkg/domain"
	"github.com/you/mongo/internal/pkg/storage"
	"github.com/google/uuid"
	"github.com/julienschmidt/httprouter"
)

type Controller struct {
	Storage storage.CommentStorer
}

// POST /api/v1/comments
func (c Controller) Create(w http.ResponseWriter, r *http.Request) {
	var req Create
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	id := uuid.New().String()

	err := c.Storage.Insert(r.Context(), storage.Comment{
		UUID:      id,
		Text:      fmt.Sprintf("%s - %d", req.Text, time.Now().UTC().Nanosecond()),
		CreatedAt: time.Now(),
	})
	if err != nil {
		switch err {
		case domain.ErrConflict:
			w.WriteHeader(http.StatusConflict)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	w.WriteHeader(http.StatusCreated)
	_, _ = w.Write([]byte(id))
}

// GET /api/v1/comments/:uuid
func (c Controller) Find(w http.ResponseWriter, r *http.Request) {
	id := httprouter.ParamsFromContext(r.Context()).ByName("uuid")

	com, err := c.Storage.Find(r.Context(), id)
	if err != nil {
		switch err {
		case domain.ErrNotFound:
			w.WriteHeader(http.StatusNotFound)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	res := Response{
		UUID:      com.UUID,
		Text:      com.Text,
		CreatedAt: com.CreatedAt,
	}

	body, err := json.Marshal(res)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write(body)
}

// DELETE /api/v1/comments/:uuid
func (c Controller) Delete(w http.ResponseWriter, r *http.Request) {
	id := httprouter.ParamsFromContext(r.Context()).ByName("uuid")

	err := c.Storage.Delete(r.Context(), id)
	if err != nil {
		switch err {
		case domain.ErrNotFound:
			w.WriteHeader(http.StatusNotFound)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// PATCH /api/v1/comments/:uuid
func (c Controller) Update(w http.ResponseWriter, r *http.Request) {
	var req Update
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	err := c.Storage.Update(r.Context(), storage.Comment{
		UUID: httprouter.ParamsFromContext(r.Context()).ByName("uuid"),
		Text: req.Text,
	})
	if err != nil {
		switch err {
		case domain.ErrNotFound:
			w.WriteHeader(http.StatusNotFound)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// GET /api/v1/comments?page=1&limit=10&sort=-created_at
func (c Controller) List(w http.ResponseWriter, r *http.Request) {
	page, err := strconv.Atoi(r.URL.Query().Get("page"))
	if err != nil || page < 1 {
		page = 1
	}
	limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
	if err != nil || limit < 1 {
		limit = 1
	}

	coms, err := c.Storage.List(r.Context(), page, limit)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	res := make([]Response, len(coms))
	for i, com := range coms {
		res[i] = Response{
			UUID:      com.UUID,
			Text:      com.Text,
			CreatedAt: com.CreatedAt,
		}
	}

	body, err := json.Marshal(res)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write(body)
}

request.go

package comment

type Create struct {
	Text string `json:"text"`
}

type Update struct {
	Text string `json:"text"`
}

response.go

package comment

import "time"

type Response struct {
	UUID      string    `json:"uuid"`
	Text      string    `json:"text"`
	CreatedAt time.Time `json:"created_at"`
}

error.go

package domain

import "errors"

var (
	ErrInternal = errors.New("internal")
	ErrNotFound = errors.New("not found")
	ErrConflict = errors.New("conflict")
)

comment_storer.go

package storage

import (
	"context"
	"time"
)

type CommentStorer interface {
	Insert(ctx context.Context, comment Comment) error
	Find(ctx context.Context, uuid string) (Comment, error)
	Delete(ctx context.Context, uuid string) error
	Update(ctx context.Context, comment Comment) error
	List(ctx context.Context, page, limit int) ([]*Comment, error)
}

type Comment struct {
	UUID      string    `bson:"uuid"`
	Text      string    `bson:"text"`
	CreatedAt time.Time `bson:"created_at"`
}

评论.go

package mongodb

import (
	"context"
	"log"
	"time"

	"github.com/you/mongo/internal/pkg/domain"
	"github.com/you/mongo/internal/pkg/storage"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

var _ storage.CommentStorer = CommentStorage{}

type CommentStorage struct {
	Database *mongo.Database
	Timeout  time.Duration
}

func (c CommentStorage) Insert(ctx context.Context, comment storage.Comment) error {
	ctx, cancel := context.WithTimeout(ctx, c.Timeout)
	defer cancel()

	if _, err := c.Database.Collection("comments").InsertOne(ctx, comment); err != nil {
		log.Println(err)

		if er, ok := err.(mongo.WriteException); ok && er.WriteErrors[0].Code == 11000 {
			return domain.ErrConflict
		}

		return domain.ErrInternal
	}

	return nil
}

func (c CommentStorage) Find(ctx context.Context, uuid string) (storage.Comment, error) {
	ctx, cancel := context.WithTimeout(ctx, c.Timeout)
	defer cancel()

	var com storage.Comment

	qry := bson.M{"uuid": uuid}

	err := c.Database.Collection("comments").FindOne(ctx, qry).Decode(&com)
	if err != nil {
		log.Println(err)

		if err == mongo.ErrNoDocuments {
			return storage.Comment{}, domain.ErrNotFound
		}

		return storage.Comment{}, domain.ErrInternal
	}

	return com, nil
}

func (c CommentStorage) Delete(ctx context.Context, uuid string) error {
	ctx, cancel := context.WithTimeout(ctx, c.Timeout)
	defer cancel()

	qry := bson.M{"uuid": uuid}

	res, err := c.Database.Collection("comments").DeleteOne(ctx, qry)
	if err != nil {
		log.Println(err)

		return domain.ErrInternal
	}
	if res.DeletedCount == 0 {
		return domain.ErrNotFound
	}

	return nil
}

func (c CommentStorage) Update(ctx context.Context, comment storage.Comment) error {
	ctx, cancel := context.WithTimeout(ctx, c.Timeout)
	defer cancel()

	qry := bson.M{"uuid": comment.UUID}
	upd := bson.M{"$set": bson.M{"text": comment.Text}}

	// If required, this replaces the whole record.
	// res, err := c.Database.Collection("comments").ReplaceOne(ctx, qry, comment)
	res, err := c.Database.Collection("comments").UpdateOne(ctx, qry, upd)
	if err != nil {
		log.Println(err)

		return domain.ErrInternal
	}
	if res.MatchedCount == 0 {
		return domain.ErrNotFound
	}

	return nil
}

func (c CommentStorage) List(ctx context.Context, page, limit int) ([]*storage.Comment, error) {
	ctx, cancel := context.WithTimeout(ctx, c.Timeout)
	defer cancel()

	var skip int
	if page > 1 {
		skip = (page - 1) * limit
	}

	qry := bson.M{}
	opt := options.FindOptions{}
	opt.SetSkip(int64(skip))
	opt.SetLimit(int64(limit))
	opt.SetSort(bson.M{"created_at": -1}) // This should come from user!

	cur, err := c.Database.Collection("comments").Find(ctx, qry, &opt)
	if err != nil {
		log.Println(err)

		return nil, domain.ErrInternal
	}

	var comments []*storage.Comment

	for cur.Next(context.Background()) {
		comment := &storage.Comment{}

		err := cur.Decode(comment)
		if err != nil {
			log.Println(err)

			return nil, domain.ErrInternal
		}

		comments = append(comments, comment)
	}
	defer cur.Close(context.Background())

	if err := cur.Err(); err != nil {
		log.Println(err)

		return nil, domain.ErrInternal
	}

	return comments, nil
}

数据库.go

package mongodb

import (
	"context"
	"fmt"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type DatabaseConfig struct {
	Auth string
	Host string
	Port string
	User string
	Pass string
	Name string
}

func NewDatabase(ctx context.Context, cnf DatabaseConfig) (*mongo.Database, error) {
	mon, err := mongo.Connect(ctx, options.Client().
		SetAuth(options.Credential{
			AuthMechanism: cnf.Auth,
			AuthSource:    cnf.Name,
			Username:      cnf.User,
			Password:      cnf.Pass,
		}).
		ApplyURI(fmt.Sprintf("mongodb://%s:%s", cnf.Host, cnf.Port)),
	)
	if err != nil {
		return nil, err
	}

	return mon.Database(cnf.Name), nil
}

测试

# Create
curl --request POST 'http://localhost:3000/api/v1/comments' \
     --header 'Content-Type: application/json' \
     --data-raw '{"text": "Hello"}'

# Update
curl --request PATCH 'http://localhost:3000/api/v1/comments/11206d6f-8a77-44bc-9442-2ef6c7f98507' \
     --header 'Content-Type: application/json' \
     --data-raw '{"text": "Hello - 1"}'

# Find
curl --request GET 'http://localhost:3000/api/v1/comments/11206d6f-8a77-44bc-9442-2ef6c7f98507'

# Delete
curl --request DELETE 'http://localhost:3000/api/v1/comments/11206d6f-8a77-44bc-9442-2ef6c7f98507'

# List
curl --request GET 'http://localhost:3000/api/v1/comments?page=1&limit=4'