sql >> Base de Datos >  >> NoSQL >> MongoDB

Paginación eficiente en MongoDB usando mgo

Lamentablemente, el mgo.v2 el controlador no proporciona llamadas API para especificar cursor.min() .

pero hay una solución. El mgo.Database type proporciona un Database.Run() método para ejecutar cualquier comando de MongoDB. Los comandos disponibles y su documentación se pueden encontrar aquí:Comandos de la base de datos

A partir de MongoDB 3.2, un nuevo find está disponible el comando que se puede usar para ejecutar consultas, y admite especificar el min argumento que denota la primera entrada de índice desde la que comenzar a enumerar los resultados.

Bien. Lo que tenemos que hacer es después de cada lote (documentos de una página) generar el min documento del último documento del resultado de la consulta, que debe contener los valores de la entrada de índice que se utilizó para ejecutar la consulta, y luego se puede adquirir el siguiente lote (los documentos de la página siguiente) configurando esta entrada de índice mínima antes para ejecutar la consulta.

Esta entrada de índice, llamémosla cursor de ahora en adelante, puede codificarse en una string y se envía al cliente junto con los resultados, y cuando el cliente quiere la siguiente página, devuelve el cursor diciendo que quiere resultados que comiencen después de este cursor.

Hacerlo manualmente (la forma "difícil")

El comando a ejecutar puede tener diferentes formas, pero el nombre del comando (find ) debe ser el primero en el resultado serializado, por lo que usaremos bson.D (que conserva el orden en contraste 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},
    )
}

El resultado de ejecutar MongoDB find comando con Database.Run() se puede capturar con el siguiente 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)
}

Ahora tenemos los resultados, pero en una porción de tipo []bson.Raw . Pero lo queremos en una porción de tipo []*User . Aquí es donde Collection.NewIter() viene bien Puede transformar (desorganizar) un valor de tipo []bson.Raw en cualquier tipo que solemos pasar a Query.All() o Iter.All() . Bien. Veámoslo:

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

Ya tenemos los usuarios de la página siguiente. Solo queda una cosa:generar el cursor que se usará para obtener la página siguiente en caso de que lo necesitemos:

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
}

Todo esto está bien, pero ¿cómo convertimos un cursorData a string ¿y viceversa? Podemos usar bson.Marshal() y bson.Unmarshal() combinado con codificación base64; el uso de base64.RawURLEncoding nos dará una cadena de cursor segura para la web, una que se puede agregar a las consultas de URL sin escapar.

Aquí hay una implementación de ejemplo:

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

Y finalmente tenemos nuestro MongoDB mgo eficiente, pero no tan corto funcionalidad de paginación. Sigue leyendo...

Usando github.com/icza/minquery (la forma "fácil")

La forma manual es bastante larga; se puede hacer general y automatizado . Aquí es donde github.com/icza/minquery entra en escena (divulgación:soy el autor ). Proporciona un contenedor para configurar y ejecutar MongoDB find comando, lo que le permite especificar un cursor, y después de ejecutar la consulta, le devuelve el nuevo cursor que se utilizará para consultar el siguiente lote de resultados. El contenedor es el MinQuery tipo que es muy similar a mgo.Query pero admite especificar min de MongoDB a través de MinQuery.Cursor() método.

La solución anterior usando minquery se parece a esto:

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

Y eso es todo. newCursor es el cursor que se utilizará para obtener el siguiente lote.

Nota n.º 1: Al llamar a MinQuery.All() , debe proporcionar los nombres de los campos del cursor, esto se usará para generar los datos del cursor (y, en última instancia, la cadena del cursor).

Nota n.º 2: Si está recuperando resultados parciales (usando MinQuery.Select() ), debe incluir todos los campos que forman parte del cursor (la entrada de índice) incluso si no tiene la intención de usarlos directamente, de lo contrario MinQuery.All() no tendrá todos los valores de los campos de cursor, por lo que no podrá crear el valor de cursor adecuado.

Consulte el documento del paquete de minquery aquí:https://godoc.org/github.com/icza/minquery, es bastante breve y, con suerte, limpio.