(本文大部分内容根据官方文档翻译而来)
环境准备
- golang 1.10+
- mongodb
- mgm
模型定义
定义
定义模型
type Book struct {
// DefaultModel adds _id, created_at and updated_at fields to the Model
mgm.DefaultModel `bson:",inline"`
Name string `json:"name" bson:"name"`
Pages int `json:"pages" bson:"pages"`
}
func NewBook(name string, pages int) *Book {
return &Book{
Name: name,
Pages: pages,
}
}
mgm 在创建表时会自动检测Model生成的Collection名称
book:=Book{}
// Print your model collection name.
collName := mgm.CollName(&book)
fmt.Println(collName) // 打印: books
如果要自定义生成Collection的名称。需要实现CollectionNameGetter
接口。
func (model *Book) CollectionName() string {
return "my_books"
}
// mgm return "my_books" collection
coll:=mgm.Coll(&Book{})
struct Tags
不知您是否注意到模型定义的struct tags。struct tags修改 Go 驱动程序的默认编组和解组行为 ,这是附加到 struct 字段的可选元数据片段。struct 标记最常见的用途是指定 BSON 文档中与 struct 字段对应的字段名称。下表描述了mongoDB 的 Go 驱动程序中的常见结构标记:
结构标签 | 描述 |
---|---|
omitempty |
如果将字段设置为对应于字段类型的零值,则不会对字段进行编组。 |
minsize |
如果字段类型是 int64、uint、uint32 或 uint64 类型,并且该字段的值可以适合带符号的 int32,则该字段将被序列化为 BSON int32 而不是 BSON int64。如果该值不适合带符号的 int32,则忽略此标记。 |
truncate |
如果字段类型是非浮点数字类型,则未编组到该字段中的 BSON 双精度将在小数点处被截断。 |
inline |
如果字段类型是 struct 或 map 字段,则该字段将在编组时展平,在解组时不展平。 |
如果没有来自结构标签的额外指令,Go Driver 将使用以下规则编组结构:
- Go Driver 仅对导出的字段进行编组和解组。
- Go Driver 使用相应结构字段的小写字母生成 BSON 密钥。
- Go 驱动程序将嵌入的结构字段编组为子文档。每个键都是字段类型的小写。
- 如果指针非 nil,Go Driver 将指针字段编组为基础类型。如果指针为 nil,则驱动程序将其编组为 BSON 空值。
- 解组时,Go Driver 跟随这些 D/M 类型映射 对于类型的字段
interface{}
。驱动程序将未编组的 BSON 文档interface{}
作为D
类型解组到字段中。
⚠️ 在MongoDB中,并没有直接对应于 Go 语言中
float32
类型的浮点数。在 MongoDB 的 BSON 数据格式中,所有浮点数默认都是以double
(双精度浮点数,对应于 Go 中的float64
)的形式存储,它是一个 64 位的 IEEE 754 浮点数。当你使用 Go 语言与 MongoDB 进行交互时,如使用官方的
mongo-go-driver
,你可以直接插入float64
类型的数据到 MongoDB 中,数据库会将它储存为double
类型。如果你的类型不慎写成float32
类型,那么很可能会出现精度上的差异问题。
模型默认字段
每个模型的都包含mgm.DefaultModel
,包含下面三个字段:
_id
: 文档 Id.created_at
: 文档创建时间. 保存文档时通过Creating
勾子自动填充。updated_at
: 文档最后更新时间. 保存文档时通过Saving
勾子自动填充。
模型勾子(Hook)
-
Creating
: Model新模型时调用.使用DefaultModel默认使用该Hook 函数签名:Creating() error
-
Created
: Model创建完成后被调用。 函数签名:Created() error
-
Updating
: Model更新时调用. 函数签名:Updating() error
-
Updated
: Model更新后被调用. 函数签名:Updated(result *mongo.UpdateResult) error
-
Saving
: Model 在creating 或者updating被调用.使用DefaultModel默认使用该Hook 函数签名:Saving() error
-
Saved
: Model 在Created 或 updated被调用. 函数签名:Saved() error
-
Deleting
: Model在 deleting时调用. 函数签名:Deleting() error
-
Deleted
: Model删除后调用. 函数签名:Deleted(result *mongo.DeleteResult) error
下面是一个使用
Creating
进行参数校验的例子:func (model *Book) Creating() error { // Call to DefaultModel Creating hook if err:=model.DefaultModel.Creating();err!=nil{ return err } // We can check if model fields is not valid, return error to // cancel document insertion . if model.Pages < 1 { return errors.New("book must have at least one page") } return nil }
注意:只有下面的几个方法可以调用hook,其他的类似xxxOne或者xxxMany不会调用hook
-
Create
&CreateWithCtx
-
Update
&UpdateWithCtx
-
Delete
&DeleteWithCtx
参考issue
-
使用
开始使用之前,先设置默认配置选项:Descending
import (
"github.com/kamva/mgm/v3"
"go.mongodb.org/mongo-driver/mongo/options"
)
func init() {
// Setup the mgm default config
err := mgm.SetDefaultConfig(nil, "test", options.Client().ApplyURI("mongodb://root:12345@localhost:27017"))
}
新增
book:=NewBook("Pride and Prejudice", 345)
// Make sure pass the model by reference.
err := mgm.Coll(book).Create(book)
如果需要设置数据在某一时间自动过期(清除),那么可以使用下面的语句:
book:=NewBook("Pride and Prejudice", 345)
model := mongo.IndexModel{
Keys: bson.D{
{"created_at", 1},
},
Options:options.Index().SetExpireAfterSeconds(t.data.temporaryRecordExpireSeconds),
}
_, err = mgm.Coll(book).Indexes().CreateOne(ctx, model)
if err != nil {
return "", "", err
}
更多详情,请参考 expire data index ttl
删除
// Just find and delete your document
err := mgm.Coll(book).Delete(book)
更新
常规更新
// Find your book
book:=findMyFavoriteBook()
// and update it
book.Name="Moulin Rouge!"
err:=mgm.Coll(book).Update(book)
upsert更新
filter := bson.D{{"type", "Oolong"}}
update := bson.D{{"$set", bson.D{{"rating", 8}}}}
opts := options.Update().SetUpsert(true)
result, err := mgm.Coll(book).UpdateOne(mgm.Ctx(), filter, update, opts)
if err != nil {
panic(err)
}
查询
基础查询
简单查询
//Get document's collection
book := &Book{}
coll := mgm.Coll(book)
// Find and decode doc to the book model.
_ = coll.FindByID("5e0518aa8f1a52b0b9410ee3", book)
// Get first doc of collection
_ = coll.First(bson.M{}, book)
// Get first doc of collection with filter
_ = coll.First(bson.M{"page":400}, book)
查询并返回列表
result := []Book{}
err := mgm.Coll(&Book{}).SimpleFind(&result, bson.M{"age": bson.M{operator.Gt: 24}})
自定义返回字段
查询并隐藏_id
字段
opts := options.FindOne().SetProjection(bson.D{{"_id", 0}})
// 如果是调用的Find方法就应该是opts := options.Find().SetProjection(bson.D{{"_id", 0}})
err := mgm.Coll(&Book{}).FindOne(nil, bson.M{}, opts)
查询并返回name
和publish_year
字段
opts := options.FindOne().SetProjection(bson.D{{"_id", 0},{"name",1},{"publish_year",1}})
// 如果是调用的Find方法就应该是opts := options.Find().SetProjection(bson.D{{"_id", 0},{"name",1},{"publish_year",1}})
err := mgm.Coll(&Book{}).FindOne(nil, bson.M{}, opts)
自定义排序
查询并设置按字段排序.1
表示升序,-1
表示降序,下面的例子是按创建时间升序排列。
opts := options.FindOne().SetSort(bson.M{"created_at": 1})
// 如果是调用的Find方法就应该是opts := options.Find().SetSort(bson.M{"created_at": 1})
err := mgm.Coll(&Book{}).FindOne(nil, bson.M{}, opts)
按多个字段进行排序
opts := options.FindOne().SetSort(bson.D{{"created_at",1},{"price",-1}})
// 如果是调用的Find方法就应该是opts := options.Find().SetSort(bson.D{{"created_at",1},{"price",-1}})
err := mgm.Coll(&Book{}).FindOne(nil, bson.M{}, opts)
分页查询
一般从传统关系型数据库转过来的一般会使用skip
+limit
的组合,但是当数据量很大的时候,这种查询会变得很慢。所以推荐使用 range query
+ limit
的方式.下面是一个简单的实现:
var lastId primitive.ObjectID
coll := mgm.Coll(&Book{})
pageOptions := options.Find()
pageOptions.SetSort(bson.M{"created_at": -1})
pageOptions.SetLimit(pageSize)
if page != 1 {
pageFind := options.Find()
pageFind.SetSort(bson.M{"created_at": -1})
pageFind.SetLimit((page - 1) * pageSize)
pageFind.SetProjection(bson.M{"_id": 1})
cur, err := coll.Find(context.Background(), bson.M{}, pageFind)
if err != nil {
return
}
var data []primitive.ObjectID{}
if err := cur.All(context.Background(), &data); err != nil {
return
}
id:= data[len(data)-1]
lastId = id
}
coll.Find(context.Background(), bson.M{"_id": bson.M{operator.Gt: lastId}}, pageOptions)
每一次分页查询,都要把当前最后的一条记录的_id
作为参数传给下一次调用。
参考文章:
- https://isotropic.co/how-to-implement-pagination-in-mongodb/
- https://www.codementor.io/@arpitbhayani/fast-and-efficient-pagination-in-mongodb-9095flbqr
- https://www.mongodb.com/blog/post/paging-with-the-bucket-pattern--part-1
- https://www.mongodb.com/blog/post/paging-with-the-bucket-pattern--part-2
索引
下面是一个改自casbin MongoDB adapter的例子
indexes := []string{"ptype", "v0", "v1", "v2", "v3", "v4", "v5"}
keysDoc := bsonx.Doc{}
for _, k := range indexes {
keysDoc = keysDoc.Append(k, bsonx.Int32(1))
}
if _, err := mgm.Coll(&CasbinRule{}).Indexes().CreateOne(context.Background(),
mongo.IndexModel{
Keys: keysDoc,
Options: options.Index().SetUnique(true),
},
); err != nil {
return err
}
可以使用下面的函数来实现当索引不存的时候创建索引:
import (
"context"
"fmt"
"github.com/kamva/mgm/v3"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func CreateIndexIfNotExist(coll *mgm.Collection, indexModel mongo.IndexModel) error {
ctx := context.Background()
// Get the index name
indexName, err := getIndexName(indexModel)
if err != nil {
return fmt.Errorf("failed to get index name: %w", err)
}
// List existing indexes
cursor, err := coll.Indexes().List(ctx)
if err != nil {
return fmt.Errorf("failed to list indexes: %w", err)
}
defer cursor.Close(ctx)
// Check if index exists
for cursor.Next(ctx) {
var index bson.M
if err := cursor.Decode(&index); err != nil {
return fmt.Errorf("failed to decode index: %w", err)
}
if index["name"] == indexName {
return nil // Index already exists
}
}
// Index doesn't exist, create it
_, err = coll.Indexes().CreateOne(ctx, indexModel)
if err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
return nil
}
func getIndexName(indexModel mongo.IndexModel) (string, error) {
if indexModel.Options != nil && indexModel.Options.Name != nil {
return *indexModel.Options.Name, nil
}
// If name is not explicitly set, MongoDB generates a name based on the keys
keys, err := indexModel.Keys.MarshalBSON()
if err != nil {
return "", fmt.Errorf("failed to marshal index keys: %w", err)
}
return string(keys), nil
}
聚合
尽管我们可以使用官方go驱动中的聚合操作,但mgm也提供了更简单的方法:
官方go驱动实现
import (
"github.com/kamva/mgm/v3"
"github.com/kamva/mgm/v3/builder"
"github.com/kamva/mgm/v3/field"
. "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Author model collection
authorColl := mgm.Coll(&Author{})
cur, err := mgm.Coll(&Book{}).Aggregate(mgm.Ctx(), A{
// S function get operators and return bson.M type.
builder.S(builder.Lookup(authorColl.Name(), "author_id", field.Id, "author")),
})
使用mgm实现
authorCollName := mgm.Coll(&Author{}).Name()
result := []Book{}
// Lookup in just single line
_ := mgm.Coll(&Book{}).SimpleAggregate(&result, builder.Lookup(authorCollName, "auth_id", "_id", "author"))
// Multi stage(mix of mgm builders and raw stages)
_ := mgm.Coll(&Book{}).SimpleAggregate(&result,
builder.Lookup(authorCollName, "auth_id", "_id", "author"),
M{operator.Project: M{"pages": 0}},
)
// Do something with result...
复杂点的例子
import (
"github.com/kamva/mgm/v3"
"github.com/kamva/mgm/v3/builder"
"github.com/kamva/mgm/v3/field"
"github.com/kamva/mgm/v3/operator"
. "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Author model collection
authorColl := mgm.Coll(&Author{})
_, err := mgm.Coll(&Book{}).Aggregate(mgm.Ctx(), A{
// S function get operators and return bson.M type.
builder.S(builder.Lookup(authorColl.Name(), "author_id", field.Id, "author")),
builder.S(builder.Group("pages", M{"books": M{operator.Push: M{"name": "$name", "author": "$author"}}})),
M{operator.Unwind: "$books"},
})
if err != nil {
panic(err)
}
另外一个例子
import (
"github.com/kamva/mgm/v3"
f "github.com/kamva/mgm/v3/field"
o "github.com/kamva/mgm/v3/operator"
"go.mongodb.org/mongo-driver/bson"
)
// Instead of hard-coding mongo operators and fields
_, _ = mgm.Coll(&Book{}).Aggregate(mgm.Ctx(), bson.A{
bson.M{"$count": ""},
bson.M{"$project": bson.M{"_id": 0}},
})
// Use predefined operators and pipeline fields.
_, _ = mgm.Coll(&Book{}).Aggregate(mgm.Ctx(), bson.A{
bson.M{o.Count: ""},
bson.M{o.Project: bson.M{f.Id: 0}},
})
事务 Transaction
- 要在默认连接上运行事务,请使用
mgm.Transaction()
函数,例如:
d := &Doc{Name: "Mehran", Age: 10}
err := mgm.Transaction(func(session mongo.Session, sc mongo.SessionContext) error {
// do not forget to pass the session's context to the collection methods.
err := mgm.Coll(d).CreateWithCtx(sc, d)
if err != nil {
return err
}
return session.CommitTransaction(sc)
})
-
要使用您的上下文运行事务,请使用
mgm.TransactionWithCtx()
方法。 -
要在另一个连接上运行事务,请使用
mgm.TransactionWithClient()
方法。
migrate
需要使用库 https://github.com/golang-migrate/migrate
Validate
参考issue
import (
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/kamva/mgm/v3"
)
// User contains user information
type User struct {
mgm.DefaultModel `bson:",inline"`
FirstName string `bson:"first_name"`
LastName string `bson:"last_name"`
Age uint8 `bson:"age"`
Email string `bson:"email"`
FavouriteColor string `bson:"favourite_color"`
}
// This hook will be called on every update or create operation.
func (user *User) Saving() error {
if err := user.DateFields.Saving(); err != nil {
return err
}
return validation.ValidateStruct(&user,
validation.Field(&user.FirstName, validation.Required),
validation.Field(&user.LastName, validation.Required),
validation.Field(&user.Age, validation.Required, validation.Min(15)),
validation.Field(&user.Email, validation.Required, is.EmailFormat),
)
}
语句监控
在 Golang 中使用 MongoDB 驱动程序时,您可以使用 MongoDB 驱动程序提供的 mongo.CommandMonitor
接口来输出查询执行的语句。通过实现这个接口,您可以接收 MongoDB 驱动程序发送的所有命令和查询,并输出它们的详细信息。
以下是一个示例代码,演示如何在 MongoDB 驱动程序中使用 mongo.CommandMonitor
接口输出查询执行的语句:
package main
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.mongodb.org/mongo-driver/mongo/event"
)
type customCommandMonitor struct {}
func (m *customCommandMonitor) Started(ctx context.Context, startedEvent *event.CommandStartedEvent) {
log.Printf("Started command: %v", startedEvent.Command)
}
func (m *customCommandMonitor) Succeeded(ctx context.Context, succeededEvent *event.CommandSucceededEvent) {
log.Printf("Succeeded command: %v", succeededEvent.Command)
}
func (m *customCommandMonitor) Failed(ctx context.Context, failedEvent *event.CommandFailedEvent) {
log.Printf("Failed command: %v, error: %v", failedEvent.Command, failedEvent.Failure)
}
func main() {
// 设置 MongoDB 连接参数
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
// 建立 MongoDB 连接
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
log.Fatal(err)
}
// 设置 MongoDB 命令监视器
clientOptions.SetMonitor(&event.CommandMonitor{
&customCommandMonitor{},
})
// 检查 MongoDB 连接是否正常
err = client.Ping(ctx, readpref.Primary())
if err != nil {
log.Fatal(err)
}
// 建立 MongoDB 数据库和集合
collection := client.Database("testdb").Collection("testcollection")
// 执行查询操作
cursor, err := collection.Find(ctx, bson.M{})
if err != nil {
log.Fatal(err)
}
// 遍历查询结果
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var result bson.M
err := cursor.Decode(&result)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
// 关闭 MongoDB 连接
err = client.Disconnect(ctx)
if err != nil {
log.Fatal(err)
}
}
在上面的示例代码中,我们定义了一个自定义的 customCommandMonitor 类型,并实现了 mongo.CommandMonitor 接口的三个方法。然后,我们将这个自定义的命令监视器设置为 MongoDB 驱动程序的监视器,以便接收并输出所有命令和查询的详细信息。
在执行查询操作时,MongoDB 驱动程序将发送 find 命令到 MongoDB 服务器,并返回查询结果。通过这个自定义的命令监视器,我们可以输出 find 命令的详细信息,包括查询条件、返回字段等。
参考资料
- 官方文档 Quick Start: Golang & MongoDB - Data Aggregation Pipeline
- Go By Example
- https://www.mongodb.com/zh-cn/docs/
- 官方GO驱动使用详解
- Custom-marshal Golang structs with flattening
- https://medium.com/@amsokol.com/new-official-mongodb-go-driver-and-google-protobuf-making-them-work-together-6357b0118f3f
- How to Implement a GraphQL API Server using Golang and MongoDB