MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Pagination efficace dans MongoDB à l'aide de mgo

Malheureusement, le mgo.v2 le pilote ne fournit pas d'appels API pour spécifier cursor.min() .

Mais il y a une solution. La mgo.Database le type fournit un Database.Run() méthode pour exécuter toutes les commandes MongoDB. Les commandes disponibles et leur documentation se trouvent ici :Commandes de la base de données

À partir de MongoDB 3.2, un nouveau find La commande est disponible et peut être utilisée pour exécuter des requêtes, et elle prend en charge la spécification du min argument qui indique la première entrée d'index à partir de laquelle commencer à lister les résultats.

Bon. Ce que nous devons faire, c'est après chaque lot (documents d'une page) générer le min document du dernier document du résultat de la requête, qui doit contenir les valeurs de l'entrée d'index qui a été utilisée pour exécuter la requête, puis le lot suivant (les documents de la page suivante) peut être acquis en définissant cette entrée d'index min avant pour exécuter la requête.

Cette entrée d'index -appelons-la curseur à partir de maintenant– peut être encodé dans une string et envoyé au client avec les résultats, et quand le client veut la page suivante, il renvoie le curseur disant qu'il veut des résultats commençant après ce curseur.

Le faire manuellement (la méthode "difficile")

La commande à exécuter peut être sous différentes formes, mais le nom de la commande (find ) doit être le premier dans le résultat marshalé, nous utiliserons donc bson.D (qui préserve l'ordre contrairement à 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},
    )
}

Le résultat de l'exécution d'un find MongoDB commande avec Database.Run() peut être capturé avec le type suivant :

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

Nous avons maintenant les résultats, mais dans une tranche de type []bson.Raw . Mais nous le voulons dans une tranche de type []*User . C'est là que Collection.NewIter() vient à portée de main. Il peut transformer (unmarshal) une valeur de type []bson.Raw dans n'importe quel type que nous passons habituellement à Query.All() ou Iter.All() . Bon. Voyons ça :

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

Nous avons maintenant les utilisateurs de la page suivante. Il ne reste plus qu'une chose :générer le curseur à utiliser pour obtenir la page suivante si jamais nous en avons besoin :

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
}

Tout cela est bien, mais comment convertir un cursorData à string et vice versa? Nous pouvons utiliser bson.Marshal() et bson.Unmarshal() combiné avec l'encodage base64 ; l'utilisation de base64.RawURLEncoding nous donnera une chaîne de curseur sécurisée pour le Web, qui peut être ajoutée aux requêtes d'URL sans échappement.

Voici un exemple d'implémentation :

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

Et nous avons enfin notre efficace, mais pas si court MongoDB mgo fonctionnalité de pagination. Lisez la suite...

En utilisant github.com/icza/minquery (la manière "facile")

La voie manuelle est assez longue; il peut être rendu général et automatisé . C'est ici que github.com/icza/minquery entre en scène (divulgation :je suis l'auteur ). Il fournit un wrapper pour configurer et exécuter un find MongoDB , vous permettant de spécifier un curseur, et après l'exécution de la requête, il vous renvoie le nouveau curseur à utiliser pour interroger le prochain lot de résultats. Le wrapper est le MinQuery type qui est très similaire à mgo.Query mais il prend en charge la spécification du min de MongoDB via le MinQuery.Cursor() méthode.

La solution ci-dessus utilisant minquery ressemble à ceci :

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

Et c'est tout. newCursor est le curseur à utiliser pour récupérer le lot suivant.

Remarque 1 : Lors de l'appel de MinQuery.All() , vous devez fournir les noms des champs du curseur, cela sera utilisé pour construire les données du curseur (et finalement la chaîne du curseur) à partir de.

Remarque n° 2 : Si vous récupérez des résultats partiels (en utilisant MinQuery.Select() ), vous devez inclure tous les champs qui font partie du curseur (l'entrée d'index) même si vous n'avez pas l'intention de les utiliser directement, sinon MinQuery.All() n'aura pas toutes les valeurs des champs de curseur, et donc il ne pourra pas créer la bonne valeur de curseur.

Consultez la documentation du package de minquery ici :https://godoc.org/github.com/icza/minquery, c'est plutôt court et j'espère propre.