За съжаление mgo.v2
драйверът не предоставя API извиквания за указване на cursor.min()
.
Но има решение. mgo.Database
type предоставя Database.Run()
метод за изпълнение на всякакви команди на MongoDB. Наличните команди и тяхната документация можете да намерите тук:Команди на базата данни
Започвайки с MongoDB 3.2, ново find
е налична команда, която може да се използва за изпълнение на заявки и поддържа задаване на min
аргумент, който обозначава първия запис в индекса, от който започва да се изброяват резултати.
Добре. Това, което трябва да направим, е след всяка партида (документи на страница) да генерираме min
документ от последния документ на резултата от заявката, който трябва да съдържа стойностите на записа в индекса, който е бил използван за изпълнение на заявката, а след това следващата партида (документите на следващата страница) може да бъде получена чрез задаване на този мин. запис на индекса преди за изпълнение на заявката.
Този индексен запис – нека го наречем курсор от сега нататък – може да се кодира в 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},
)
}
Резултатът от изпълнение на 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
. Но ние го искаме в отрязък от тип []*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
ще ни даде безопасен в мрежата низ на курсора, който може да се добавя към 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, доста е кратък и да се надяваме чист.