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.