Tyvärr mgo.v2
drivrutinen tillhandahåller inte API-anrop för att specificera cursor.min()
.
Men det finns en lösning. mgo.Database
typ ger en Database.Run()
metod för att köra alla MongoDB-kommandon. De tillgängliga kommandona och deras dokumentation finns här:Databaskommandon
Från och med MongoDB 3.2, en ny find
kommandot är tillgängligt som kan användas för att köra frågor, och det stöder specificering av min
argument som anger den första indexposten att börja lista resultat från.
Bra. Vad vi behöver göra är att efter varje batch (dokument på en sida) generera min
dokument från det sista dokumentet i frågeresultatet, som måste innehålla värdena för indexposten som användes för att köra frågan, och sedan kan nästa batch (dokumenten på nästa sida) hämtas genom att ställa in denna min indexpost innan för att köra frågan.
Denna indexpost – låt oss kalla den markör från och med nu– kan kodas till en string
och skickas till klienten tillsammans med resultaten, och när klienten vill ha nästa sida skickar han tillbaka markören säger att han vill ha resultat som börjar efter den här markören.
Gör det manuellt (det "svåra" sättet)
Kommandot som ska köras kan ha olika former, men kommandots namn (find
) måste vara först i det samlade resultatet, så vi använder bson.D
(vilket bevarar ordningen i motsats till 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},
)
}
Resultatet av att köra en MongoDB find
kommando med Database.Run()
kan fångas med följande typ:
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)
}
Vi har nu resultaten, men i en del av typen []bson.Raw
. Men vi vill ha den i en skiva av typen []*User
. Det är här Collection.NewIter()
kommer väl till pass. Den kan transformera (unmarshal) ett värde av typen []bson.Raw
till vilken typ som helst som vi vanligtvis skickar till Query.All()
eller Iter.All()
. Bra. Låt oss se det:
firstBatch := res.Cursor.FirstBatch
var users []*User
err = db.C("users").NewIter(nil, firstBatch, 0, nil).All(&users)
Vi har nu användare av nästa sida. Bara en sak kvar:generera markören som ska användas för att få nästa sida om vi någonsin skulle behöva den:
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
}
Det här är bra, men hur konverterar vi en cursorData
till string
och vice versa? Vi kan använda bson.Marshal()
och bson.Unmarshal()
kombinerat med base64-kodning; användningen av base64.RawURLEncoding
kommer att ge oss en webbsäker markörsträng, en som kan läggas till i URL-frågor utan att behöva undkomma.
Här är ett exempel på implementering:
// 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
}
Och vi har äntligen vår effektiva, men inte så korta MongoDB mgo
personsökningsfunktion. Läs vidare...
Använder github.com/icza/minquery
(det "enkla" sättet)
Den manuella vägen är ganska lång; det kan göras allmänt och automatiserad . Det är här github.com/icza/minquery
kommer in i bilden (avslöjande:Jag är författaren ). Den tillhandahåller ett omslag för att konfigurera och exekvera en MongoDB find
kommando, så att du kan ange en markör, och efter att du har kört frågan, ger den dig tillbaka den nya markören som ska användas för att fråga nästa grupp resultat. Omslaget är MinQuery
typ som är mycket lik mgo.Query
men det stöder att specificera MongoDB:s min
via MinQuery.Cursor()
metod.
Ovanstående lösning med minquery
ser ut så här:
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")
Och det är allt. newCursor
är markören som ska användas för att hämta nästa batch.
Obs #1: När du anropar MinQuery.All()
, du måste ange namnen på markörfälten, detta kommer att användas för att bygga markördata (och i slutändan markörsträngen) från.
Obs #2: Om du hämtar partiella resultat (genom att använda MinQuery.Select()
), måste du inkludera alla fält som är en del av markören (indexposten) även om du inte tänker använda dem direkt, annars MinQuery.All()
kommer inte att ha alla värden för markörfälten, så det kommer inte att kunna skapa rätt markörvärde.
Kolla in paketdokumentet för minquery
här:https://godoc.org/github.com/icza/minquery, den är ganska kort och förhoppningsvis ren.