在Golang中使用MongoDB的一对多关系在子文档中保留父文档的引用

90 阅读2分钟

假设你有一个 "用户 "和 "日志 "集合。一个用户可以有很多日志。正如你所想象的,这是一个可变的操作,你不能保证一个用户可能有多少条日志。它可能是1个,也可能是一百万个!在这种情况下,理想的做法是将父集合的引用存储在子集合中。所以在这种情况下,日志中包含用户的引用。请看下面的例子。

数据库内容

准备工作

db.createCollection("users")
db.users.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})

db.createCollection("logs")
db.logs.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})
db.logs.createIndex({"user_uuid":1},{name:"IX_user_uuid"})
db.logs.createIndex({"created_at":1},{name:"IX_created_at"})

用户数据

[
  {
    "_id": {
      "$oid": "604fb496d42b77e01e7f03e0"
    },
    "uuid": "1ee18d53-a3fc-4493-9ca6-029a4890437e",
    "name": "John"
  },
  {
    "_id": {
      "$oid": "604fb496d42b77e01e7f03e1"
    },
    "uuid": "a977db4a-7007-4147-b7bd-8d9565ef3dd7",
    "name": "Andy"
  },
  {
    "_id": {
      "$oid": "604fb496d42b77e01e7f03e2"
    },
    "uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc",
    "name": "Robert"
  }
]

日志数据

[
  {
    "_id": {
      "$oid": "604fb7f2d42b77e01e7f03e3"
    },
    "uuid": "0e5e800d-e8fb-4bfe-aaee-29d3c3b480ea",
    "action": "login",
    "created_at": {
      "$date": "2021-03-15T19:39:30.252Z"
    },
    "user_uuid": "1ee18d53-a3fc-4493-9ca6-029a4890437e"
  },
  {
    "_id": {
      "$oid": "604fb7f2d42b77e01e7f03e4"
    },
    "uuid": "318277c2-c4a8-4c3a-8dc9-dc170d2cf6fc",
    "action": "login",
    "created_at": {
      "$date": "2021-03-15T19:39:30.252Z"
    },
    "user_uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc"
  },
  {
    "_id": {
      "$oid": "604fb7f2d42b77e01e7f03e5"
    },
    "uuid": "dc10a79b-1b47-4b13-944e-9c9bb31fdb8c",
    "action": "logout",
    "created_at": {
      "$date": "2021-03-15T19:39:31.252Z"
    },
    "user_uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc"
  }
]

存储

模型

package storage

import "context"

type UserStorer interface {
	Find(ctx context.Context, uuid string) (UserRead, error)
}

type UserRead struct {
	ID   string `bson:"_id"`
	UUID string `bson:"uuid"`
	Name string `bson:"name"`
	Logs []Log  `bson:"logs"`
}

type Log struct {
	ID        string    `bson:"_id"`
	UUID      string    `bson:"uuid"`
	Action    string    `bson:"action"`
	CreatedAt time.Time `bson:"created_at"`
	UserUUID  string    `bson:"user_uuid"`
}

存储器

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"
)

var _ storage.UserStorer = UserStorage{}

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

func (u UserStorage) Find(ctx context.Context, uuid string) (storage.UserRead, error) {
	ctx, cancel := context.WithTimeout(ctx, u.Timeout)
	defer cancel()

	// Logs are desc sorted here!
	qry := []bson.M{
		{
			"$match": bson.M{
				"uuid": uuid,
			},
		},
		{
			"$lookup": bson.M{
				// Define the logs collection for the join.
				"from": "logs",
				"pipeline": []bson.M{
					// Select only the relevant logs from the logs collection.
					{
						"$match": bson.M{
							"user_uuid": uuid,
						},
					},
					// Sort logs by their created_at field in desc. 1 = asc
					{
						"$sort": bson.M{
							"created_at": -1,
						},
					},
				},
				// Use logs as the field name to match struct field.
				"as": "logs",
			},
		},
	}

	cur, err := u.Database.Collection("users").Aggregate(ctx, qry)
	if err != nil {
		log.Println(err)

		return storage.UserRead{}, domain.ErrInternal
	}

	var usr []storage.UserRead

	if err := cur.All(context.Background(), &usr); err != nil {
		log.Println(err)

		return storage.UserRead{}, domain.ErrInternal
	}
	defer cur.Close(context.Background())

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

		return storage.UserRead{}, domain.ErrInternal
	}

	if len(usr) == 0 {
		return storage.UserRead{}, domain.ErrNotFound
	}

	return usr[0], nil
}

// This is for listing.
// qry := []bson.M{
//     {
//         "$lookup": bson.M{
//             "from": "logs",
//             "let": bson.M{
//                 "uuid": "$uuid",
//             },
//             "pipeline": []bson.M{
//                 {
//                     "$match": bson.M{
//                         "$expr": bson.M{
//                             "$eq": []interface{}{
//                                 "$user_uuid",
//                                 "$$uuid",
//                             },
//                         },
//                     },
//                 },
//                 {
//                     "$sort": bson.M{
//                         "created_at": -1,
//                     },
//                 },
//             },
//             "as": "logs",
//         },
//     },
// }

HTTP 路由器

模型

package user

import "time"

type Response struct {
	ID   string `json:"id"`
	UUID string `json:"uuid"`
	Name string `json:"name"`
	Logs []Log  `json:"logs"`
}

type Log struct {
	ID        string    `json:"id"`
	UUID      string    `json:"uuid"`
	Action    string    `json:"action"`
	CreatedAt time.Time `json:"created_at"`
}

控制器

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

package user

import (
	"encoding/json"
	"net/http"

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

type Controller struct {
	Storage storage.UserStorer
}

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

	usr, 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{
		ID:   usr.ID,
		UUID: usr.UUID,
		Name: usr.Name,
		Logs: make([]Log, len(usr.Logs)),
	}
	for i, log := range usr.Logs {
		res.Logs[i].ID = log.ID
		res.Logs[i].UUID = log.UUID
		res.Logs[i].Action = log.Action
		res.Logs[i].CreatedAt = log.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)
}

测试

curl --request GET 'http://localhost:3000/api/v1/users/40507244-d612-4fa4-b93f-c9c692f3b0fc'

{
  "id": "604fb496d42b77e01e7f03e2",
  "uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc",
  "name": "Robert",
  "logs": [
    {
      "id": "604fb7f2d42b77e01e7f03e5",
      "uuid": "dc10a79b-1b47-4b13-944e-9c9bb31fdb8c",
      "action": "logout",
      "created_at": "2021-03-15T19:39:31.252Z"
    },
    {
      "id": "604fb7f2d42b77e01e7f03e4",
      "uuid": "318277c2-c4a8-4c3a-8dc9-dc170d2cf6fc",
      "action": "login",
      "created_at": "2021-03-15T19:39:30.252Z"
    }
  ]
}