我搜索并没有找到解决该问题的Go解决方案,无论是否使用mgo.v2,都没有,也没有在其他任何站点上。
mgo.v2
假设我们users在Gogo中建模了一个MongoDB集合struct:
users
struct
type User struct { ID bson.ObjectId `bson:"_id"` Name string `bson:"name"` Country string `bson:"country"` }
我们希望根据某些条件对用户进行排序和列出,但是由于预期结果列表较长,因此已实现了分页。
为了实现对某些查询结果的分页,MongoDB和mgo.v2驱动程序包以Query.Skip()和的形式内置支持Query.Limit(),例如:
Query.Skip()
Query.Limit()
session, err := mgo.Dial(url) // Acquire Mongo session, handle error! c := session.DB("").C("users") q := c.Find(bson.M{"country" : "USA"}).Sort("name", "_id").Limit(10) // To get the nth page: q = q.Skip((n-1)*10) var users []*User err = q.All(&users)
但是,如果页数增加,这将变得很慢,因为MongoDB不能仅“神奇地”跳到结果中的第x 个文档,它必须遍历所有结果文档并忽略(不返回)x需要删除的第一个文档跳过。
x
MongoDB提供了正确的解决方案:如果查询对索引进行操作(必须对索引进行操作),cursor.min()则可用于指定第一个 索引条目 以开始列出结果。
cursor.min()
注意:以上查询所需的索引为:
db.users.createIndex( { country: 1, name: 1, _id: 1 } )
但是,有一个问题:mgo.v2软件包不支持指定this min()。
min()
我们如何cursor.min()通过mgo.v2驱动程序使用MongoDB的功能来实现有效的分页?
不幸的是,mgo.v2驱动程序没有提供API调用来指定cursor.min()。
但是有一个解决方案。该mgo.Database类型提供了Database.Run()一种运行任何MongoDB命令的方法。可在此处找到可用的命令及其文档:数据库命令
mgo.Database
Database.Run()
从MongoDB 3.2开始,提供了一个新find命令,该命令可用于执行查询,并且支持指定min表示第一个索引条目的参数,该索引条目开始列出结果。
find
min
好。我们需要做的是在每批(一页min文档)从查询结果的最后一个文档生成文档之后,该文档必须包含用于执行查询的索引条目的值,然后是下一个批次(可以通过在执行查询之前设置此最小索引条目来获取下一页的文档)。
该索引条目(从现在开始将其称为 光标) 可以被编码为a string并与结果一起发送给客户端,并且当客户端想要下一页时,他会发回 光标, 说他想要从该光标之后开始的结果。
string
要执行的命令可以采用不同的形式,但是命令名称(find)必须在经过编组的结果中位于第一个位置,因此我们将使用bson.D(与相比保留了顺序bson.M):
bson.D
bson.M
limit := 10 cmd := bson.D{ {Name: "find", Value: "users"}, {Name: "filter", Value: bson.M{"country": "USA"}}, {Name: "sort", Value: []bson.D{ {Name: "name", Value: 1}, {Name: "_id", Value: 1}, }, {Name: "limit", Value: limit}, {Name: "batchSize", Value: limit}, {Name: "singleBatch", Value: true}, } if min != nil { // min is inclusive, must skip first (which is the previous last) cmd = append(cmd, bson.DocElem{Name: "skip", Value: 1}, bson.DocElem{Name: "min", Value: min}, ) }
可以使用以下类型捕获执行MongoDB find命令的结果Database.Run():
var res struct { OK int `bson:"ok"` WaitedMS int `bson:"waitedMS"` Cursor struct { ID interface{} `bson:"id"` NS string `bson:"ns"` FirstBatch []bson.Raw `bson:"firstBatch"` } `bson:"cursor"` } db := session.DB("") if err := db.Run(cmd, &res); err != nil { // Handle error (abort) }
现在,我们得到结果,但类型为[]bson.Raw。但是我们希望它是type的一部分[]*User。这是Collection.NewIter()方便的地方。它可以将类型的值转换(解组)为[]bson.Raw我们通常传递给Query.All()或的任何类型Iter.All()。好。让我们来看看它:
[]bson.Raw
[]*User
Collection.NewIter()
Query.All()
Iter.All()
firstBatch := res.Cursor.FirstBatch var users []*User err = db.C("users").NewIter(nil, firstBatch, 0, nil).All(&users)
现在,我们有了下一页的用户。只剩下一件事了:如果需要,生成用于获取下一页的游标:
if len(users) > 0 { lastUser := users[len(users)-1] cursorData := []bson.D{ {Name: "country", Value: lastUser.Country}, {Name: "name", Value: lastUser.Name}, {Name: "_id", Value: lastUser.ID}, } } else { // No more users found, use the last cursor }
一切都很好,但是我们如何将a转换cursorData为string,反之亦然?我们可能会使用bson.Marshal()和bson.Unmarshal()用base64编码相结合; 的使用base64.RawURLEncoding将为我们提供一个网络安全的游标字符串,可以将其添加到URL查询中而不进行转义。
cursorData
bson.Marshal()
bson.Unmarshal()
base64.RawURLEncoding
这是一个示例实现:
// CreateCursor returns a web-safe cursor string from the specified fields. // The returned cursor string is safe to include in URL queries without escaping. func CreateCursor(cursorData bson.D) (string, error) { // bson.Marshal() never returns error, so I skip a check and early return // (but I do return the error if it would ever happen) data, err := bson.Marshal(cursorData) return base64.RawURLEncoding.EncodeToString(data), err } // ParseCursor parses the cursor string and returns the cursor data. func ParseCursor(c string) (cursorData bson.D, err error) { var data []byte if data, err = base64.RawURLEncoding.DecodeString(c); err != nil { return } err = bson.Unmarshal(data, &cursorData) return }
最后,我们有了高效但不那么短的MongoDB mgo分页功能。继续阅读…
mgo
github.com/icza/minquery
手动方式相当冗长;它可以 通用 和 自动化 。这就是github.com/icza/minquery图片所在的位置( 披露:我是作者 )。它提供了一个包装器来配置和执行MongoDB find命令,允许您指定一个游标,并且在执行查询后,它会返回给您新的游标,以用于查询下一批结果。包装器的MinQuery类型非常相似,mgo.Query但是它支持min通过MinQuery.Cursor()方法指定MongoDB的类型。
MinQuery
mgo.Query
MinQuery.Cursor()
上面使用的解决方案minquery如下所示:
minquery
q := minquery.New(session.DB(""), "users", bson.M{"country" : "USA"}). Sort("name", "_id").Limit(10) // If this is not the first page, set cursor: // getLastCursor() represents your logic how you acquire the last cursor. if cursor := getLastCursor(); cursor != "" { q = q.Cursor(cursor) } var users []*User newCursor, err := q.All(&users, "country", "name", "_id")
就这样。newCursor是用于获取下一批的光标。
newCursor
注意#1: 调用时MinQuery.All(),必须提供游标字段的名称,这将用于从中构建游标数据(最终是游标字符串)。
MinQuery.All()
注意#2: 如果您要检索部分结果(使用MinQuery.Select()),则即使您不打算直接使用它们,也必须包括所有属于光标的字段(索引条目),否则MinQuery.All()就不会游标字段的所有值,因此它将无法创建正确的游标值。
MinQuery.Select()
在minquery此处查看打包文档:https : //godoc.org/github.com/icza/minquery,它很短,希望可以清理。