使用mgo在MongoDB中进行高效分页

时间:2016-11-16 14:38:08

标签: mongodb go pagination mongodb-query mgo

我已经搜索过,没有找到问题的Go解决方案,没有使用或不使用mgo.v2,而不是使用StackOverflow而不是任何其他网站。这个Q& A符合knowledge sharing / documenting的精神。

我们假设我们在MongoDB中有一个users集合,使用此Go struct建模:

type User struct {
    ID      bson.ObjectId `bson:"_id"`
    Name    string        `bson:"name"`
    Country string        `bson:"country"`
}

我们希望根据某些条件对用户进行排序和列出,但由于预期的长结果列表而实施了分页。

为了实现对某些查询结果的分页,MongoDB和mgo.v2驱动程序包具有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 th 文档,它必须遍历所有结果文档并省略(不返回)需要跳过的第一个x

MongoDB提供了正确的解决方案:如果查询对索引进行操作(它必须处理索引),cursor.min()可用于指定第一个索引条目以开始列出结果来自。

此Stack Overflow答案显示了如何使用mongo客户端完成:How to do pagination using range queries in MongoDB?

注意:上述查询所需的索引为:

db.users.createIndex(
    {
        country: 1,
        name: 1,
        _id: 1
    }
)

但是有一个问题:mgo.v2包没有指定此min()的支持。

我们如何使用cursor.min()驱动程序实现使用MongoDB mgo.v2功能的高效分页?

1 个答案:

答案 0 :(得分:17)

不幸的是,mgo.v2驱动程序未提供API调用来指定cursor.min()

但是有一个解决方案。 mgo.Database类型提供了Database.Run()方法来运行任何MongoDB命令。可以在此处找到可用的命令及其文档:Database commands

从MongoDB 3.2开始,可以使用新的find命令来执行查询,并且它支持指定min参数,该参数表示开始列出结果的第一个索引条目。 / p>

好。我们需要做的是在每个批处理(页面文档)之后从查询结果的最后一个文档生成min文档,该文档必须包含用于执行查询的索引条目的值,以及然后可以通过在执行查询之前设置此最小索引条目来获取下一批(下一页的文档)。

此索引条目-let从现在开始称之为 cursor - 可以编码为string并与结果一起发送给客户端,当客户端需要下一页时,他发回光标,说他希望在此光标后开始结果。

手动完成(“硬”方式)

要执行的命令可以采用不同的形式,但命令名称(find)必须在编组结果中排在第一位,因此我们将使用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},
    )
}

使用以下类型可以捕获使用find执行MongoDB 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类型的切片中。但我们希望它在[]*User类型的片段中。这是Collection.NewIter()方便的地方。它可以将[]bson.Raw类型的值转换(解组)为我们通常传递给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
}

这一切都很好,但我们如何将cursorData转换为string,反之亦然?我们可以将bson.Marshal()bson.Unmarshal()结合使用base64编码; base64.RawURLEncoding的使用将为我们提供一个Web安全的游标字符串,可以在不转义的情况下添加到URL查询中。

以下是一个示例实现:

// 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分页功能。请继续阅读...

使用github.com/icza/minquery(“简单”方式)

手动方式相当漫长;它可以是通用自动。这是github.com/icza/minquery进入图片的地方(披露:我是作者)。它提供了一个包装器来配置和执行MongoDB find命令,允许您指定游标,并在执行查询后,它会返回用于查询下一批结果的新游标。包装器是MinQuery类型,与mgo.Query非常相似,但它支持通过min方法指定MongoDB的MinQuery.Cursor()

使用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是用于获取下一批的游标。

注意#1:调用MinQuery.All()时,必须提供游标字段的名称,这将用于构建游标数据(最终是游标字符串)

注意#2:如果您正在检索部分结果(使用MinQuery.Select()),则必须包括属于游标的所有字段(索引条目)如果你不想直接使用它们,那么MinQuery.All()将不会拥有光标字段的所有值,因此它将无法创建正确的光标值。

在这里查看minquery的包文档:https://godoc.org/github.com/icza/minquery,它很短,希望很干净。