Paginação
Três maneiras de percorrer uma coleção: um offset, um número de página e um cursor. Os dois primeiros são posicionais e o terceiro não é, o que faz toda a diferença — uma página posicional relê contando a partir do início, então linhas gravadas enquanto você pagina deslocam o que “linha 20” significa.
// Offset-based paginationconst page1 = await client.data.products.find({ limit: 20, offset: 0 });const page2 = await client.data.products.find({ limit: 20, offset: 20 });
// Check if more pages existif (page1.meta.hasMore) { // fetch next page}
// Page-number pagination (1-indexed)const page = await client.data.products.find({ page: 2, limit: 20 });Use um ou outro. Desde 0.25 page e offset juntos descrevem a mesma janela
duas vezes, e a leitura é recusada com PAGE_WITH_OFFSET em vez de aplicar
silenciosamente page. O SDK a recusa antes de qualquer requisição, por HTTP, em processo e
offline; o servidor responde com um 400 a uma requisição crua que envie os dois.
limit deve ser um número inteiro entre 1 e 1000. Um valor maior — ou zero,
negativo, ou fracionário — é recusado com um 400 INVALID_LIMIT em vez de ser
ajustado (clamped), porque uma página silenciosamente menor não pode ser diferenciada da última.
Para ler além desse teto, percorra as páginas com iterate() ou findAll().
Paginação por cursor
Seção intitulada “Paginação por cursor”Toda resposta de lista carrega um meta.nextCursor enquanto houver outra página.
Passe-o de volta como after e a próxima página continuará estritamente após a última linha
retornada, em vez de em uma contagem de linhas que gravações simultâneas já
deslocaram:
let after: string | undefined;do { const { data, meta } = await client.data.orders.find({ orderBy: ["createdAt", "desc"], limit: 100, after }); for (const order of data) await handle(order); after = meta.nextCursor;} while (after);nextCursor está ausente na última página, e em uma página que nenhum cursor
pode descrever: uma busca ordenada por relevância (_score), uma busca vetorial
e uma projeção fields que deixa de fora um dos campos de orderBy — o cursor
é construído a partir dos valores de ordenação da última linha, e essa linha não
os carrega. Ali, meta.hasMore pode ser true sem nenhum nextCursor, e o loop
acima para antes do fim: adicione os campos de ordenação a fields, ou pagine
por offset. O iterate(), ao receber um cursor, recusa essa listagem com
cursor-missing em vez de parar. Uma ordenação por um
agregado sobre uma relação pagina por cursor normalmente.
O cursor é opaco. Ele codifica as chaves de ordenação e os valores da última linha
para elas, de modo que ele só pode continuar a listagem da qual veio: mantenha o orderBy
idêntico entre as páginas, ou a requisição será recusada com
CURSOR_ORDER_MISMATCH em vez de buscar em uma ordem que ninguém solicitou. Uma
requisição que não define nenhum orderBy adota o do cursor, para que você possa passá-lo
diretamente de volta sem precisar redefinir a ordenação.
Não faça o parse dele e não construa um: a codificação existe para ser alterada, e
qualquer outra coisa resultará em INVALID_CURSOR.
Quatro consequências decorrem do que um cursor é:
afternão pode ser combinado comoffsetoupage(400CURSOR_WITH_OFFSET). Ambos informam onde a página começa, e honrar ambos pularia linhas.- Ordenações por múltiplas chaves e chaves que aceitam nulo funcionam. A comparação é construída sobre
cada chave em ordem, com o posicionamento de NULL que a ordenação
declarou — não um único
>em uma coluna. - Relevância não pode ser um cursor. Um
_scoreé calculado por consulta e não é armazenado em lugar nenhum, e duas consultas com strings de busca diferentes produzem pontuações que não estão na mesma escala. Tal listagem simplesmente não carreganextCursor; pagine-a comoffset. O mesmo vale para a distância de uma busca vetorial, e um cursor enviado com uma é recusado comVECTOR_CURSOR_UNSUPPORTED. - Uma assinatura não pode continuar a partir de um cursor.
listen()reexecuta sua consulta a cada escrita, eafterindica um ponto em uma única execução dela, entãolisten({ after })é recusado comCURSOR_NOT_LIVEantes de qualquer envio. Useoffsetoupagepara uma janela ao vivo, oufind({ after })para ler a página uma vez.
Via HTTP, é um único parâmetro:
GET /api/data/orders?orderBy=createdAt:desc&limit=100GET /api/data/orders?orderBy=createdAt:desc&limit=100&after=eyJrIjpbWyJ…Ignorando a contagem
Seção intitulada “Ignorando a contagem”Desde 0.25 meta.total é um COUNT(*) sobre todas as linhas que a consulta encontra, executado ao lado da
página — e numa tabela grande custa mais do que a própria página. Uma leitura que só pagina para a frente nunca precisa dele, porque
meta.hasMore não depende dele: passe count: "none" e a leitura pede uma linha além da página.
const { data, meta } = await client.data.orders.find({ limit: 100, count: "none" });meta.hasMore; // exactSem contagem não há total a informar: via HTTP, meta.total está ausente,
e no mesmo processo ele são as linhas até o fim da página (offset mais a
página). Via HTTP é ?count=none; ?count=exact é o padrão, e qualquer
outro valor resulta em um 400 INVALID_COUNT. iterate() e findAll() o enviam
em cada página por conta própria, já que um percurso não informa total.
Quais leituras são envelopadas e quais não são
Seção intitulada “Quais leituras são envelopadas e quais não são”Dois formatos e uma regra: uma janela é envelopada, uma resposta completa não é.
| Método | Retorna | Por quê |
|---|---|---|
find(), listen() |
{ data, meta } |
Uma página. meta.total / meta.hasMore são a única forma de saber se há mais |
findAll(), createMany(), updateMany() |
M[] |
Nada restante para relatar — o percurso terminou ou o lote são as linhas |
iterate() |
uma linha por vez | Nada é materializado |
findById(), get(), create(), update() |
uma linha | Não é uma lista |
data não é um wrapper que o SDK às vezes adiciona e às vezes esquece. É
onde os metadados de paginação residem, e está presente exatamente quando há algum.
Lendo tudo: iterate() e findAll()
Seção intitulada “Lendo tudo: iterate() e findAll()”iterate() transmite cada linha correspondente a uma consulta, uma por vez, buscando uma página de
cada vez nos bastidores. Nada se acumula, portanto é o método a ser usado para uma
coleção que você não pode manter na memória:
for await (const order of client.data.orders.iterate({ where: { status: ["==", "pending"] }})) { await handleOrder(order);}findAll() é o mesmo percurso coletado em um array:
const stale = await client.data.sessions.findAll({ where: { expiresAt: ["<", cutoff] }});Ambos também estão no fluent builder, onde .limit() se torna o tamanho da página
em vez de um total:
const rows = await client.data.orders .where("status", "==", "pending") .orderBy("createdAt", "asc") .limit(500) // rows per request .findAll();Quatro opções moldam o percurso:
| Opção | Padrão | O que faz |
|---|---|---|
pageSize |
200 | Linhas por requisição. |
cursor |
— | Exige a busca: lança um erro em vez de recorrer ao offset. Veja abaixo. |
maxPages |
10 000 | Teto de requisições, para que um servidor que nunca para de retornar hasMore não fique em loop infinito. |
maxRows |
10 000 | Apenas findAll(). Exceder esse valor lança um erro em vez de retornar um array truncado como se fosse a resposta completa. Passe Infinity para desativar, ou use iterate(). |
O percurso faz a busca por conta própria. Depois da primeira página, ele devolve
o cursor do servidor — meta.nextCursor — como after,
de modo que cada requisição pede linhas estritamente após a última visualizada. Gravações
simultâneas antes dessa linha não podem deslocar a janela, e o banco de dados nunca precisa
pular linhas por contagem, que era o que tornava lentos os percursos longos por offset: cada página custava
mais do que a anterior.
Onde o servidor não emite cursor — uma busca ordenada por relevância, uma busca
vetorial, uma leitura com distinct, uma projeção de fields que deixa de fora um campo
de ordenação — o percurso pagina por offset, e uma linha
inserida ou excluída enquanto o percurso é executado pode fazê-lo pular ou repetir uma linha.
Passe cursor para tornar a busca uma exigência em vez de uma preferência:
for await (const job of client.data.jobs.iterate({ cursor: "id" })) { /* … */ }cursor define a coluna pela qual ordenar quando a consulta ainda não especificar, e
transforma o recurso ao offset em um erro. O percurso não constrói nenhuma comparação
própria, razão pela qual uma ordenação por múltiplas chaves
funciona —
for await (const job of client.data.jobs.iterate({ cursor: "id", orderBy: [["priority", "desc"], ["createdAt", "asc"]]})) { /* … */ }— e por que uma chave de ordenação que aceita nulos também funciona.
A ordenação ainda precisa ser total, o que na prática significa única: o id da linha
desempata no final, de modo que qualquer coluna funciona como critério de desempate. Com cursor, um
percurso cujo cursor para de avançar lança cursor-stalled em vez de entrar em loop
infinito, e uma consulta que nenhum cursor consegue descrever (relevância) lança
cursor-missing; remova cursor e o percurso as pagina por offset.
Veja também
Seção intitulada “Veja também”- Consultando dados — filtros, o fluent builder, ordenação.
- Agregações e busca — por que a relevância não pode ser chave de um cursor.