MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Paginação eficiente no MongoDB usando mgo


Infelizmente, o mgo.v2 driver não fornece chamadas de API para especificar cursor.min() .

Mas há uma solução. O mgo.Database type fornece um Database.Run() método para executar qualquer comando do MongoDB. Os comandos disponíveis e sua documentação podem ser encontrados aqui:Comandos de banco de dados

A partir do MongoDB 3.2, um novo find está disponível o comando que pode ser usado para executar consultas e suporta a especificação do min argumento que denota a primeira entrada de índice para começar a listar os resultados.

Bom. O que precisamos fazer é após cada lote (documentos de uma página) gerar o min documento do último documento do resultado da consulta, que deve conter os valores da entrada de índice que foi usada para executar a consulta, e então o próximo lote (os documentos da próxima página) pode ser adquirido definindo essa entrada de índice mínimo antes para executar a consulta.

Esta entrada de índice – vamos chamá-la de cursor a partir de agora – pode ser codificado para uma string e enviado ao cliente junto com os resultados, e quando o cliente quiser a próxima página, ele devolve o cursor dizendo que ele quer que os resultados comecem após este cursor.

Fazendo manualmente (da maneira "difícil")


O comando a ser executado pode ter diferentes formas, mas o nome do comando (find ) deve ser o primeiro no resultado empacotado, então usaremos bson.D (que preserva a ordem em contraste com 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},
    )
}

O resultado da execução de um find do MongoDB comando com Database.Run() pode ser capturado com o seguinte 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)
}

Agora temos os resultados, mas em uma fatia do tipo []bson.Raw . Mas queremos em uma fatia do tipo []*User . É aqui que Collection.NewIter() vem a calhar. Ele pode transformar (unmarshal) um valor do tipo []bson.Raw em qualquer tipo que normalmente passamos para Query.All() ou Iter.All() . Bom. Vamos ver isso:
firstBatch := res.Cursor.FirstBatch
var users []*User
err = db.C("users").NewIter(nil, firstBatch, 0, nil).All(&users)

Agora temos os usuários da próxima página. Só resta uma coisa:gerar o cursor a ser usado para obter a página subsequente, caso precisemos:
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
}

Isso é tudo de bom, mas como convertemos um cursorData para string e vice versa? Podemos usar bson.Marshal() e bson.Unmarshal() combinado com codificação base64; o uso de base64.RawURLEncoding nos fornecerá uma string de cursor segura para a Web, que pode ser adicionada a consultas de URL sem escapar.

Aqui está um exemplo de implementação:
// 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 temos nosso eficiente, mas não tão curto MongoDB mgo funcionalidade de paginação. Leia...

Usando github.com/icza/minquery (a maneira "fácil")


O caminho manual é bastante demorado; pode ser feito geral e automatizado . É aqui que github.com/icza/minquery entra em cena (divulgação:eu sou o autor ). Ele fornece um wrapper para configurar e executar um find do MongoDB comando, permitindo que você especifique um cursor, e depois de executar a consulta, ele devolve o novo cursor a ser usado para consultar o próximo lote de resultados. O wrapper é o MinQuery tipo que é muito semelhante a mgo.Query mas suporta especificar o min do MongoDB através do MinQuery.Cursor() método.

A solução acima usando minquery se parece com isso:
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 isso é tudo. newCursor é o cursor a ser usado para buscar o próximo lote.

Observação nº 1: Ao chamar MinQuery.All() , você deve fornecer os nomes dos campos do cursor, isso será usado para construir os dados do cursor (e, finalmente, a string do cursor).

Observação nº 2: Se você estiver recuperando resultados parciais (usando MinQuery.Select() ), você deve incluir todos os campos que fazem parte do cursor (a entrada do índice), mesmo que não pretenda usá-los diretamente, senão MinQuery.All() não terá todos os valores dos campos de cursor e, portanto, não poderá criar o valor de cursor adequado.

Confira a documentação do pacote de minquery aqui:https://godoc.org/github.com/icza/minquery, é bastante curto e esperançosamente limpo.