概述

本教程将介绍如何使用官方的 Golang 驱动 Mongo Go Driver 连接 MongoDB,操作 MongoDB 以及索引的一些操作。更多的是一些使用方法,当然,还有一些注意点也是有提到,其实也是踩过的一些坑了。

API 使用

导入依赖包

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    // 官方的 "mongo-go-driver "包
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/x/bsonx"
)

连接 DB

使用 options.Client() 创建一个 MongoDB 连接的参数选项,你可以添加各种你需要选项,同时 MongoDB 的 URI 信息也是通过这里加入:

[root@liqiang.io]# cat main.go
// 声明要传递给 Connect() 方法的主机和端口选项
var opts = options.Client().
        ApplyURI(addr).
        SetConnectTimeout(time.Second * 5).
        SetSocketTimeout(time.Second * 5).
        SetWriteConcern(writeconcern.New(writeconcern.WMajority()))

这里我使用了很多参数:

然后执行以下命令,将 clientOptions 实例传递给 mongo.Connect() 方法,并确保同时需要传递一个 context 对象,这个 context 你可以做很多事情,例如控制连接超时超时时间之类的:

[root@liqiang.io]# cat main.go
// 连接到MongoDB并返回客户端实例
客户端,err := mongo.Connect(context.TODO(), clientOptions)
if err !=nil {
    fmt.Println("mongo.Connect() ERROR:", err)
    os.Exit(1)
}

操作 Collection

有了 Client 之后,还需要指定 DB 和 Collection 才能操作具体的 Collection,例如:

[root@liqiang.io]# cat main.go
// 通过数据库访问 MongoDB 集合
col := client.Database("db").Collection("collection")

CRUD 就太简单了,直接看例子吧。

CRUD

Get

Get 方法的要点其实就是 FindOne 返回的值是 SingleResult,然后一些常见的错误可以处理一下:

[root@liqiang.io]# cat main.go
func (r *postRepository) Get(ctx context.Context, id string) (post *postDoc, err error) {
    var coll = r.client.Database(r.dbName).Collection(r.collName)

    var singleResult *mongo.SingleResult
    if singleResult = coll.FindOne(ctx, bson.M{"post_id": id}); singleResult.Err() != nil {
        if singleResult.Err() == mongo.ErrNoDocuments {
            return nil, utils.ErrNotFound
        }
        return nil, errors.Wrap(singleResult.Err(), "query doc")
    }

    var doc postDoc
    if err = singleResult.Decode(&doc); err != nil {
        return nil, errors.Wrap(err, "decode doc")
    }

    return &doc, nil
}

List

List 就比较复杂了,主要有几个要点:

[root@liqiang.io]# cat main.go
func (r *postRepository) List(
    ctx context.Context,
    selector map[string]interface{},
    sorter []string,
    page, pageSize int,
) (posts []postDoc, err error) {
    var coll = r.client.Database(r.dbName).Collection(r.collName)

    var cursor *mongo.Cursor
    var sortOpts = bsonx.Doc{}
    for _, s := range sorter {
        var order = int32(1)
        if strings.HasPrefix(s, "-") {
            order = int32(-1)
            s = s[1:]
        }
        sortOpts = append(sortOpts, bsonx.Elem{
            Key:   s,
            Value: bsonx.Int32(order),
        })
    }
    var opts = options.Find().
        SetSort(sortOpts).
        SetSkip(int64((page - 1) * pageSize)).
        SetLimit(int64(pageSize))
    if cursor, err = coll.Find(ctx, selector, opts); err != nil {
        return nil, errors.Wrap(err, "query docs")
    }

    var postDocs []postDoc
    if err = cursor.All(ctx, &postDocs); err != nil {
        return nil, errors.Wrap(err, "decode docs")
    }
    return postDocs, nil
}

这里我使用的是 cursor.All 来解压数据,还有一种比较常见的用法是这样的:

[root@liqiang.io]# cat main.go
    var postDocs []postDoc
    defer cursor.Close(ctx)
    for cursor.Next(ctx) {
        var postDoc postDoc
        if err = cursor.Decode(&postDoc); err != nil {
            return nil, errors.Wrap(err, "decode doc")
        }
        postDocs = append(postDocs, postDoc)
    }

这里是采用迭代器的形式来进行解压数据的,使用这种方法切记一定要主动关闭 Cursor,否则可能造成连接泄漏的情况

Delete

删除的话比较简单,唯一一点值得一提的就是删除的元素不存在怎么判断,这里我是通过判断删除结果中的:DeleteCount 来判断是否真的删除了元素:

[root@liqiang.io]# cat main.go
func (r *postRepository) Delete(ctx context.Context, id string) (err error) {
    var coll = r.client.Database(r.dbName).Collection(r.collName)

    var delRst *mongo.DeleteResult
    if delRst, err = coll.DeleteOne(ctx, bson.M{"post_id": id}); err != nil {
        return errors.Wrap(err, "delete doc")
    }

    if delRst.DeletedCount == 0 {
        return utils.ErrNotFound
    }
    return nil
}

Update

更新也是比较简单的操作,一个需要注意的点就是是否允许 upsert,这里是设置了这个选项:

[root@liqiang.io]# cat main.go
func (r *postRepository) Update(ctx context.Context, post *postDoc) (rtn *postDoc, err error) {
    var coll = r.client.Database(r.dbName).Collection(r.collName)

    var opts = options.Update().SetUpsert(true)
    _, err = coll.UpdateOne(ctx, bson.M{"post_id": post.PostId}, bson.M{"$set": post}, opts)
    if err != nil {
        return nil, errors.Wrap(err, "upsert doc")
    }

    return post, nil
}

Create

创建元素的话有一个点就是插入元素的 ID 是什么,在 Mongo Go Driver 中是通过一个返回值来确定的,但是返回的是一个 interface{},需要进行转换:

[root@liqiang.io]# cat main.go
func (r *postRepository) Save(ctx context.Context, post *postDoc) (rtn *postDoc, err error) {
    var coll = r.client.Database(r.dbName).Collection(r.collName)

    var insertOneResult *mongo.InsertOneResult
    if insertOneResult, err = coll.InsertOne(ctx, post); err != nil {
        return nil, errors.Wrap(err, "save doc")
    }
    post.Id = insertOneResult.InsertedID.(primitive.ObjectID)

    return post, nil
}

索引

创建单个索引

无论使用哪种语言,使用最新的驱动创建索引的方法调用都是 createIndex()createOne()create_index() 方法,如果是 Go 的官方驱动 API,则是 Indexes().CreateOne 方法。这个方法调用需要传递一个 key,或者是用来索引数据的字段,以及一个排序顺序的整数,可以是升序的 1,也可以是降序的 -1。第二个参数的选项也可能是必需的。下面是一个例子。

[root@liqiang.io]# db.coll.createIndex( |CONTEXT|, { |KEY| : |SORT_ORDER|, |OPTIONS| } )

这个是 Github 上 mongo-go-driverindex_view.go, 这个方法的方法原型:

// CreateOne 在模型指定的集合中创建一个索引。
func (iv IndexView) CreateOne(ctx context.Context, model IndexModel, opts, *options.CreateIndexesOptions) (string, error)

可选择将 options.Index() 方法调用传递给IndexModel的 Options 参数。下面的例子就是为博客的 id 设置一个唯一索引。

func (r *postRepository) createIndex(ctx context.Context) (err error) {
    var coll = r.client.Database(r.dbName).Collection(r.collName)
    var indexModel = mongo.IndexModel{
        Keys: bsonx.Doc{
            {
                Key: "post_id",
            },
        },
        Options: options.Index().SetUnique(true),
    }
    var createOpts = options.CreateIndexes().SetMaxTime(time.Second * 10)
    if _, err = coll.Indexes().CreateOne(ctx, indexModel, createOpts); err != nil {
        return errors.Wrap(err, "create unique post_id index")
    }
    return nil
}

同时创建多个索引

创建多个索引和创建单个索引的区别就两个:

其他地方都是类似的。

[root@liqiang.io]# cat main.go
func (r *postRepository) createIndexes(ctx context.Context) (err error) {
    var coll = r.client.Database(r.dbName).Collection(r.collName)
    var indexModels = []mongo.IndexModel{
        {
            Keys: bsonx.Doc{
                {
                    Key: "post_id",
                },
            },
            Options: options.Index().SetUnique(true),
        },
        {
            Keys: bsonx.Doc{
                {
                    Key:   "type",
                    Value: bsonx.Int32(1),
                },
            },
        },
        {
            Keys: bsonx.Doc{
                {
                    Key:   "status",
                    Value: bsonx.Int32(1),
                },
            },
        },
    }
    var createOpts = options.CreateIndexes().SetMaxTime(time.Second * 10)
    if _, err = coll.Indexes().CreateMany(ctx, indexModels, createOpts); err != nil {
        return errors.Wrap(err, "create indexes")
    }
    return nil
}