Paginación
Tres formas de recorrer una colección: un offset, un número de página y un cursor. Las dos primeras son posicionales y la tercera no, lo cual marca toda la diferencia: una página posicional vuelve a leer contando desde el inicio, por lo que las filas escritas mientras paginas alteran lo que significa la «fila 20».
// 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 });Usa uno u otro. Desde 0.25 page y offset juntos describen la misma ventana
dos veces, y la lectura se rechaza con PAGE_WITH_OFFSET en lugar de aplicar en
silencio page. El SDK lo rechaza antes de cualquier solicitud, por HTTP, en proceso y
sin conexión; el servidor responde con un 400 a una solicitud directa que envíe ambos.
limit debe ser un número entero entre 1 y 1000. Un valor mayor —o cero, negativo o fraccionario— se rechaza con un 400 INVALID_LIMIT en lugar de ajustarse automáticamente, ya que una página silenciosamente más pequeña no se puede distinguir de la última. Para leer más allá de ese límite superior, recorre las páginas con iterate() o findAll().
Paginación por cursor
Sección titulada «Paginación por cursor»Cada respuesta de lista incluye un meta.nextCursor mientras exista otra página. Envíalo de vuelta como after y la siguiente página continuará estrictamente después de la última fila servida, en lugar de basarse en un conteo de filas que las escrituras concurrentes ya hayan desplazado:
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 en la última página, y en una página que ningún
cursor puede describir: una búsqueda ordenada por relevancia (_score), una
búsqueda vectorial y una proyección fields que omite uno de los campos de
orderBy — el cursor se construye a partir de los valores de ordenación de la
última fila, y esa fila no los lleva. Ahí meta.hasMore puede ser true sin
ningún nextCursor, y el bucle anterior se detiene antes de tiempo: añade los
campos de ordenación a fields, o pagina por offset. iterate() con un
cursor rechaza ese listado con cursor-missing en lugar de detenerse. Una
ordenación por una agregación sobre una relación sí
pagina por cursor.
El cursor es opaco. Codifica las claves de ordenación y los valores de la última fila para ellas, por lo que solo puede continuar la lista de la que provino: mantén orderBy idéntico en todas las páginas, o la solicitud será rechazada con CURSOR_ORDER_MISMATCH en lugar de buscarse en un orden que nadie solicitó. Una solicitud que no especifique ningún orderBy adopta el del cursor, por lo que puedes reenviarlo directamente sin tener que volver a definir el orden.
No intentes analizarlo ni construir uno: la codificación existe sujeta a cambios, y cualquier otra cosa generará un INVALID_CURSOR.
De la naturaleza de un cursor se desprenden cuatro cosas:
afterno se puede combinar conoffsetopage(400CURSOR_WITH_OFFSET). Ambos definen dónde comienza la página, y respetar ambos omitiría filas.- Tanto las ordenaciones por múltiples claves como las claves que admiten nulos funcionan. La comparación se construye sobre cada clave en orden, con la ubicación de los NULL que declaró la ordenación, no un simple
>sobre una sola columna. - La relevancia no puede ser un cursor. El
_scorese calcula por consulta y no se almacena en ninguna parte, y dos consultas con diferentes cadenas de búsqueda producen puntuaciones que no están en la misma escala. Dicho listado simplemente no incluyenextCursor; pagínalo conoffset. La distancia de una búsqueda vectorial es igual, y un cursor enviado con una se rechaza conVECTOR_CURSOR_UNSUPPORTED. - Una suscripción no puede continuar desde un cursor.
listen()vuelve a ejecutar su consulta con cada escritura, yafternombra un punto en una ejecución de ella, así quelisten({ after })se rechaza conCURSOR_NOT_LIVEantes de enviar nada. Usaoffsetopagepara una ventana en vivo, ofind({ after })para leer la página una vez.
A través de HTTP es un solo parámetro:
GET /api/data/orders?orderBy=createdAt:desc&limit=100GET /api/data/orders?orderBy=createdAt:desc&limit=100&after=eyJrIjpbWyJ…Omitir el conteo
Sección titulada «Omitir el conteo»Desde 0.25 meta.total es un COUNT(*) sobre todas las filas que coinciden con la consulta, ejecutado junto
a la página — y en una tabla grande cuesta más que la página. Una lectura que solo pagina hacia
delante nunca lo necesita, porque meta.hasMore no depende de él: pasa count: "none" y la
lectura pide en su lugar una fila más allá de la página.
const { data, meta } = await client.data.orders.find({ limit: 100, count: "none" });meta.hasMore; // exactSin conteo no hay total que informar: a través de HTTP meta.total está ausente, y en proceso es
el número de filas hasta el final de la página (offset más la página). A través de HTTP es
?count=none; ?count=exact es el valor por defecto, y cualquier otro valor es un 400
INVALID_COUNT. iterate() y findAll() lo envían por sí solos en cada página, ya que un
recorrido no informa de ningún total.
Qué lecturas están envueltas y cuáles no
Sección titulada «Qué lecturas están envueltas y cuáles no»Dos formas y una regla: una ventana está envuelta, una respuesta completa no.
| Método | Devuelve | Por qué |
|---|---|---|
find(), listen() |
{ data, meta } |
Una página. meta.total / meta.hasMore son la única forma de saber si hay más |
findAll(), createMany(), updateMany() |
M[] |
Nada pendiente por reportar: el recorrido finalizó o el lote son las filas |
iterate() |
una fila a la vez | No se materializa nada en absoluto |
findById(), get(), create(), update() |
una fila | No es una lista |
data no es un contenedor que el SDK a veces añade y a veces olvida. Es donde reside la metadata de paginación, y está ahí exactamente cuando hay información disponible.
Leerlo todo: iterate() y findAll()
Sección titulada «Leerlo todo: iterate() y findAll()»iterate() transmite mediante streaming cada fila que coincide con una consulta, una a la vez, obteniendo una página a la vez en segundo plano. No acumula nada, por lo que es la opción a utilizar para una colección que no puedes mantener en memoria:
for await (const order of client.data.orders.iterate({ where: { status: ["==", "pending"] }})) { await handleOrder(order);}findAll() es el mismo recorrido recopilado en un array:
const stale = await client.data.sessions.findAll({ where: { expiresAt: ["<", cutoff] }});Ambos también están disponibles en el constructor fluido, donde .limit() pasa a ser el tamaño de página en lugar de un total:
const rows = await client.data.orders .where("status", "==", "pending") .orderBy("createdAt", "asc") .limit(500) // rows per request .findAll();Cuatro opciones configuran el recorrido:
| Opción | Por defecto | Qué hace |
|---|---|---|
pageSize |
200 | Filas por solicitud. |
cursor |
— | Exige la búsqueda por cursor: lanza un error en lugar de recurrir al offset. Ver más abajo. |
maxPages |
10 000 | Límite máximo de solicitudes, para que un servidor que nunca deja de responder hasMore no entre en un bucle infinito. |
maxRows |
10 000 | Solo en findAll(). Superarlo lanza un error en lugar de devolver un array truncado como si fuera la respuesta completa. Pasa Infinity para desactivar este límite o usa iterate(). |
El recorrido busca por cursor por sí solo. Tras la primera página devuelve
el cursor del servidor —meta.nextCursor— como after, de modo que
cada solicitud pide filas estrictamente posteriores a la última vista. Las escrituras
concurrentes anteriores a esa fila no pueden desplazar la ventana, y la base de datos nunca
tiene que saltarse filas por conteo, que era lo que hacía lentos los recorridos largos por
offset: cada página costaba más que la anterior.
Donde el servidor no emite ningún cursor —una búsqueda ordenada por relevancia, una búsqueda
vectorial, una lectura distinct, una proyección fields que deja fuera un campo de
ordenación—, el recorrido pagina por offset, y una fila insertada o eliminada mientras se
ejecuta el recorrido puede hacer que omita o repita una fila. Pasa cursor para que la búsqueda
por cursor sea un requisito en lugar de una preferencia:
for await (const job of client.data.jobs.iterate({ cursor: "id" })) { /* … */ }cursor indica la columna por la cual ordenar cuando la consulta no lo especifica previamente, y
hace que recurrir al offset sea un error. El recorrido no construye ninguna comparación propia,
razón por la cual funciona la ordenación por múltiples claves —
for await (const job of client.data.jobs.iterate({ cursor: "id", orderBy: [["priority", "desc"], ["createdAt", "asc"]]})) { /* … */ }— y por qué también funciona una clave de ordenación que admite nulos.
La ordenación aún debe ser total, lo que en la práctica significa única: el id de la fila resuelve el desempate final, por lo que cualquier columna funciona como criterio de desempate. Con cursor, un recorrido cuyo cursor deja de avanzar lanza cursor-stalled en lugar de entrar en un bucle infinito, y una consulta que ningún cursor puede describir (como la relevancia) lanza cursor-missing; elimina cursor y el recorrido pagina esas consultas mediante offset.
Véase también
Sección titulada «Véase también»- Consulta de datos — filtros, el constructor fluido, ordenación.
- Agregaciones y búsqueda — por qué la relevancia no puede servir como clave para un cursor.