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
,此时的 username
和 password
对应的是 MongoDB 超级管理员,
如果对应的 username
和 password
不是超级管理员,而是某一个 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
控制的。分页通过 FindOptions
的 Skip
和 Limit
来实现:
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 端却是不行的。