MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Effizientes Paging in MongoDB mit mgo

Leider die mgo.v2 Der Treiber stellt keine API-Aufrufe bereit, um cursor.min() anzugeben .

Aber es gibt eine Lösung. Die mgo.Database Typ stellt ein Database.Run() bereit -Methode zum Ausführen von MongoDB-Befehlen. Die verfügbaren Befehle und ihre Dokumentation finden Sie hier:Datenbankbefehle

Beginnend mit MongoDB 3.2, ein neuer find Befehl verfügbar, der zum Ausführen von Abfragen verwendet werden kann, und er unterstützt die Angabe von min Argument, das den ersten Indexeintrag angibt, ab dem mit der Auflistung der Ergebnisse begonnen werden soll.

Gut. Was wir tun müssen, ist nach jedem Stapel (Dokumente einer Seite) den min zu generieren Dokument aus dem letzten Dokument des Abfrageergebnisses, das die Werte des Indexeintrags enthalten muss, der zum Ausführen der Abfrage verwendet wurde, und dann kann der nächste Stapel (die Dokumente der nächsten Seite) erfasst werden, indem dieser minimale Indexeintrag vorangestellt wird zum Ausführen der Abfrage.

Dieser Indexeintrag – nennen wir ihn Cursor von nun an – kann zu einem string kodiert werden und zusammen mit den Ergebnissen an den Client gesendet, und wenn der Client die nächste Seite möchte, sendet er den Cursor zurück sagt, er möchte Ergebnisse, die nach diesem Cursor beginnen.

Manuell (auf die "harte" Art)

Der auszuführende Befehl kann in verschiedenen Formen vorliegen, aber der Befehlsname (find ) muss im gemarshallten Ergebnis an erster Stelle stehen, also verwenden wir bson.D (was im Gegensatz zu bson.M die Ordnung bewahrt ):

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

Das Ergebnis der Ausführung eines MongoDB-find Befehl mit Database.Run() kann mit dem folgenden Typ erfasst werden:

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

Wir haben jetzt die Ergebnisse, aber in einem Slice vom Typ []bson.Raw . Aber wir wollen es in einem Slice vom Typ []*User . Hier ist Collection.NewIter() kommt praktisch. Es kann einen Wert vom Typ []bson.Raw umwandeln (unmarshalieren). in einen beliebigen Typ übergeben wir normalerweise an Query.All() oder Iter.All() . Gut. Mal sehen:

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

Wir haben jetzt die Benutzer der nächsten Seite. Bleibt nur noch eine Sache:den Cursor zu generieren, der verwendet wird, um die nächste Seite zu erhalten, falls wir sie jemals brauchen sollten:

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
}

Das ist alles gut, aber wie konvertieren wir ein cursorData zu string und umgekehrt? Wir können bson.Marshal() verwenden und bson.Unmarshal() kombiniert mit base64-Kodierung; die Verwendung von base64.RawURLEncoding gibt uns eine websichere Cursorzeichenfolge, die ohne Escapezeichen zu URL-Abfragen hinzugefügt werden kann.

Hier ist eine Beispielimplementierung:

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

Und wir haben endlich unsere effiziente, aber nicht so kurze MongoDB mgo Paging-Funktionalität. Weiterlesen...

Verwendung von github.com/icza/minquery (der "einfache" Weg)

Der manuelle Weg ist recht langwierig; es kann allgemein gemacht werden und automatisiert . Hier ist github.com/icza/minquery kommt ins Bild (Offenlegung:Ich bin der Autor ). Es bietet einen Wrapper zum Konfigurieren und Ausführen eines MongoDB-find Befehl, mit dem Sie einen Cursor angeben können, und nach dem Ausführen der Abfrage erhalten Sie den neuen Cursor zurück, der zum Abfragen des nächsten Stapels von Ergebnissen verwendet werden kann. Der Wrapper ist die MinQuery Typ, der mgo.Query sehr ähnlich ist aber es unterstützt die Angabe von min von MongoDB über MinQuery.Cursor() Methode.

Die obige Lösung mit minquery sieht so aus:

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

Und das ist alles. newCursor ist der Cursor, der zum Abrufen des nächsten Stapels verwendet werden soll.

Anmerkung 1: Beim Aufruf von MinQuery.All() , müssen Sie die Namen der Cursorfelder angeben, diese werden verwendet, um die Cursordaten (und letztendlich die Cursorzeichenfolge) daraus zu erstellen.

Anmerkung 2: Wenn Sie Teilergebnisse abrufen (mithilfe von MinQuery.Select() ), müssen Sie alle Felder einschließen, die Teil des Cursors (des Indexeintrags) sind, auch wenn Sie nicht beabsichtigen, sie direkt zu verwenden, sonst MinQuery.All() enthält nicht alle Werte der Cursorfelder und kann daher nicht den richtigen Cursorwert erstellen.

Sehen Sie sich die Paketdokumentation von minquery an hier:https://godoc.org/github.com/icza/minquery, es ist ziemlich kurz und hoffentlich sauber.