MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Impaginazione efficiente in MongoDB utilizzando mgo

Sfortunatamente il mgo.v2 driver non fornisce chiamate API per specificare cursor.min() .

Ma c'è una soluzione. Il mgo.Database type fornisce un Database.Run() metodo per eseguire qualsiasi comando MongoDB. I comandi disponibili e la relativa documentazione possono essere trovati qui:Comandi del database

A partire da MongoDB 3.2, un nuovo find è disponibile un comando che può essere utilizzato per eseguire query e supporta la specifica del min argomento che denota la prima voce dell'indice da cui iniziare a elencare i risultati.

Bene. Quello che dobbiamo fare è dopo che ogni batch (documenti di una pagina) genera il min documento dall'ultimo documento del risultato della query, che deve contenere i valori della voce di indice che è stata utilizzata per eseguire la query, quindi è possibile acquisire il batch successivo (i documenti della pagina successiva) impostando questa voce di indice minima prima per eseguire la query.

Questa voce di indice, chiamiamola cursore d'ora in poi– può essere codificato in una string e inviato al cliente insieme ai risultati, e quando il cliente desidera la pagina successiva, rinvia il cursore dicendo che vuole che i risultati inizino dopo questo cursore.

Fallo manualmente (il modo "difficile")

Il comando da eseguire può essere in diverse forme, ma il nome del comando (find ) deve essere il primo nel risultato sottoposto a marshalling, quindi useremo bson.D (che mantiene l'ordine in contrasto con 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},
    )
}

Il risultato dell'esecuzione di un MongoDB find comando con Database.Run() può essere catturato con il seguente tipo:

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)
}

Ora abbiamo i risultati, ma in una slice di tipo []bson.Raw . Ma lo vogliamo in una sezione di tipo []*User . Qui è dove Collection.NewIter() viene a portata di mano. Può trasformare (unmarshal) un valore di tipo []bson.Raw in qualsiasi tipo che di solito passiamo a Query.All() o Iter.All() . Bene. Vediamolo:

firstBatch := res.Cursor.FirstBatch
var users []*User
err = db.C("users").NewIter(nil, firstBatch, 0, nil).All(&users)

Ora abbiamo gli utenti della pagina successiva. Rimane solo una cosa:generare il cursore da utilizzare per ottenere la pagina successiva in caso di necessità:

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
}

Va tutto bene, ma come si converte un cursorData a string e viceversa? Possiamo usare bson.Marshal() e bson.Unmarshal() combinato con la codifica base64; l'uso di base64.RawURLEncoding ci fornirà una stringa del cursore sicura per il Web, che può essere aggiunta alle query URL senza caratteri di escape.

Ecco un esempio di implementazione:

// 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
}

E finalmente abbiamo il nostro efficiente, ma non così breve MongoDB mgo funzionalità di paging. Continua a leggere...

Utilizzo di github.com/icza/minquery (il modo "facile")

Il modo manuale è piuttosto lungo; può essere reso generale e automatizzato . Qui è dove github.com/icza/minquery entra in scena (rilevamento:io sono l'autore ). Fornisce un wrapper per configurare ed eseguire un MongoDB find comando, che consente di specificare un cursore e, dopo aver eseguito la query, restituisce il nuovo cursore da utilizzare per interrogare il successivo batch di risultati. Il wrapper è il MinQuery tipo che è molto simile a mgo.Query ma supporta la specifica di min di MongoDB tramite il MinQuery.Cursor() metodo.

La soluzione sopra utilizzando minquery assomiglia a questo:

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")

E questo è tutto. newCursor è il cursore da utilizzare per recuperare il batch successivo.

Nota n. 1: Quando si chiama MinQuery.All() , devi fornire i nomi dei campi del cursore, questo verrà utilizzato per costruire i dati del cursore (e infine la stringa del cursore).

Nota n. 2: Se stai recuperando risultati parziali (usando MinQuery.Select() ), devi includere tutti i campi che fanno parte del cursore (la voce dell'indice) anche se non intendi utilizzarli direttamente, altrimenti MinQuery.All() non avrà tutti i valori dei campi del cursore, quindi non sarà in grado di creare il valore corretto del cursore.

Controlla il pacchetto doc di minquery qui:https://godoc.org/github.com/icza/minquery, è piuttosto breve e, si spera, pulito.