(本文大部分内容根据官方文档翻译而来)

环境准备

  • 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 将使用以下规则编组结构:

  1. Go Driver 仅对导出的字段进行编组和解组。
  2. Go Driver 使用相应结构字段的小写字母生成 BSON 密钥。
  3. Go 驱动程序将嵌入的结构字段编组为子文档。每个键都是字段类型的小写。
  4. 如果指针非 nil,Go Driver 将指针字段编组为基础类型。如果指针为 nil,则驱动程序将其编组为 BSON 空值。
  5. 解组时,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)

查询并返回namepublish_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作为参数传给下一次调用。

参考文章:

索引

下面是一个改自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 命令的详细信息,包括查询条件、返回字段等。

参考资料