Go MongoDB

Table of Contents

建议使用 mongo-go-driver,目前已经 v1.5.1。mgo.v2 我也用过一段时间,但是目前已经不在维护了。

1. 连接报错

connection() error occured during connection handshake: auth error: sasl conversation error: unable to authenticate using mechanism "SCRAM-SHA-1": (AuthenticationFailed) Authentication failed.

MongoDB 的权限比较复杂,我也折腾不明白。不过报这个错,肯定是因为权限的问题。

在连接 MongoDB 时,URI 格式一般为: mongodb://<username>:<password>@localhost:27017 ,此时的 usernamepassword 对应的是 MongoDB 超级管理员, 如果对应的 usernamepassword 不是超级管理员,而是某一个 database 的权限,则会报如上的错。解决办法是在 uri 中携带 database 的名字。如: mongodb://<username>:<password>@localhost:27017/<database> 1

2. interface{} decode to bson.M

业务场景是这样的:在插入 MongoDB 时,会插入一个前端给的 JSON,而后端对 JSON 的具体格式并不确定,希望怎么插入就怎么输出。 所以,后端(Go)就声明成了一个 interface{} ,insert 之后没有任何问题,在 MongoDB 中存储的也达到了期望。

但是在查询时,发现返回的值并不是插入的 JSON,而被拆解成了 {"Key": "xxx", "Value": "yyy"} 这种方式,而实际上的格式是: {"xxx": "yyy"}

有人给 MongoDB 提过 issue, https://jira.mongodb.org/browse/GODRIVER-988 ,问题是相同的。解决办法是:在初始连接 MongoDB 的时候设置解析方式,

tM := reflect.TypeOf(bson.M{})
reg := bson.NewRegistryBuilder().RegisterTypeMapEntry(bsontype.EmbeddedDocument, tM).Build()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri).SetRegistry(reg))

查看 RegisterTypeMapEntry 的定义,发现在它已经在注释中说明了解析逻辑:

// RegisterTypeMapEntry will register the provided type to the BSON type. The primary usage for this
// mapping is decoding situations where an empty interface is used and a default type needs to be
// created and decoded into.
//
// By default, BSON documents will decode into interface{} values as bson.D. To change the default type for BSON
// documents, a type map entry for bsontype.EmbeddedDocument should be registered. For example, to force BSON documents
// to decode to bson.Raw, use the following code:
//  rb.RegisterTypeMapEntry(bsontype.EmbeddedDocument, reflect.TypeOf(bson.Raw{}))
func (rb *RegistryBuilder) RegisterTypeMapEntry(bt bsontype.Type, rt reflect.Type) *RegistryBuilder {
    rb.typeMap[bt] = rt
    return rb
}

默认情况下,BSON 文档的 interface{} 值是 bson.D ,即 bson.D{{"foo", "bar"}, {"hello", "world"}, {"pi", 3.14159}} 这种一对一对的形式。 而我们期望的是 bson.M ,格式是 bson.M{"foo": "bar", "hello": "world", "pi": 3.14159} 。所以改成 bson.M 就可以了。

如果希望是字节流的话,可以改成 bson.Raw{}

3. 分页、排序

MongoDB 在查询时的选项都是有 FindOptions 控制的。分页通过 FindOptionsSkipLimit 来实现:

  • Limit 表示当前页的大小
  • Skip 表示跳过多少个元素

排序通过 Sort 来实现。整体如下:

options.Find().SetSkip(skip).SetLimit(limit).SetSort(bson.M{"create_timestamp": -1})

4. 只取 Document 中的一部分

很多时候,把 Document 中的所有的字段全部取出来性能是很低下的,而且真实的业务场景也是需要其中的部分字段。

FindOptions 中的 Projection 是用来限制查询返回的字段的,默认值为 nil,即全部。 true 表示字段显示,反之 false 表示不显示。

如: options.Find().SetProjection(bson.M{"timestamp": true}) 表示只显示返回 timestamp 字段(返回的结构依然是存储的结构,只不过其它的字段全部为 nil)。

5. 存储时间时区问题

MongoDB 在存储的时候本地时间会自动转换成 UTC 时间。在 Mongo cli 查询需要 ISODate 加上本地时区做转换。 比如 ISODate("2020-10-10T11:43:06.027+-8:00") ,在 Go 代码中 insert 和 query 要保持一致的 locale。

有个地方要注意 time.Now() 是本地 locale,但是 time.Parse 并不是,它的结果是 UTC 时间, 使用 time.ParseInLocation 替代即可。

6. mongo-go-driver 构建索引报错:

(BadValue) Invalid field specified for createIndexes command: maxTimeMS

代码如下:

func yieldIndexModel(key string, unique bool, order int) mongo.IndexModel {
    keys := bsonx.Doc{{Key: key, Value: bsonx.Int32(int32(order))}}
    index := mongo.IndexModel{
        Keys: keys,
    }
    if unique {
        index.Options = options.Index().SetUnique(true)
    }
    return index
}

func (sc *StorageClient) BuildIndex(name string, key string, unique bool) error {
    db, err := sc.GetDatabase()
    if err != nil {
        return err
    }

    collection := db.Collection(name)
    opts := options.CreateIndexes().SetMaxTime(20 * time.Second) // set max build time
    indexModel := yieldIndexModel(key, unique, ASCENDING)
    indexName, err := collection.Indexes().CreateOne(context.Background(), indexModel, opts)

    // ...
}

去掉 SetMaxTime 之后就正常了。

有一个 issue 说明这个问题, maxTimeMS 只适合只读操作,是一个遗留选项,应该是不同的版本支持不同,这也就印证了在我 macOS 本地是可以的,在 server 端却是不行的。

Footnotes:

First created: 2020-03-17 16:32:37
Last updated: 2021-11-15 Mon 10:18
Power by Emacs 27.2 (Org mode 9.4.6)