Paginazione
Ci sono tre modi per scorrere una collezione: un offset, un numero di pagina e un cursore. I primi due sono posizionali e il terzo non lo è, ed è qui che risiede l’intera differenza: una pagina posizionale rilegge contando dall’inizio, quindi le righe scritte durante la paginazione modificano il significato di “riga 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 l’uno o l’altro. Da 0.25 page e offset insieme descrivono la stessa finestra
due volte, e la lettura viene rifiutata con PAGE_WITH_OFFSET anziché applicare
in silenzio page. L’SDK la rifiuta prima di qualsiasi richiesta, via HTTP, in-process e
offline; il server risponde con un 400 a una richiesta grezza che li invia entrambi.
limit deve essere un numero intero compreso tra 1 e 1000. Un valore maggiore — o pari a zero,
negativo o frazionario — viene rifiutato con un 400 INVALID_LIMIT invece di essere
bloccato a un valore limite (clamped), perché una pagina silenziosamente più piccola non può essere distinta dall’ultima.
Per leggere oltre questa soglia, scorri le pagine con iterate() o findAll().
Paginazione con cursore
Sezione intitolata “Paginazione con cursore”Ogni risposta di elenco contiene un meta.nextCursor quando è presente un’altra pagina.
Rinvialo come after e la pagina successiva ripartirà strettamente dopo l’ultima riga
restituita, invece che a un conteggio di righe che scritture concorrenti
potrebbero aver già modificato:
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 è assente nell’ultima pagina, e in una pagina che nessun cursore può
descrivere: una ricerca ordinata per rilevanza (_score), una ricerca vettoriale
e una proiezione fields che esclude uno dei campi di orderBy — il cursore
viene costruito dai valori di ordinamento dell’ultima riga, e quella riga non li
porta con sé. In quel caso meta.hasMore può essere true senza nextCursor, e
il ciclo sopra si interrompe in anticipo: aggiungi i campi di ordinamento a
fields, oppure pagina con offset. iterate() invocato con un cursor
rifiuta un simile elenco con cursor-missing invece di fermarsi. Un ordinamento
per un aggregato su una relazione pagina invece
correttamente tramite cursore.
Il cursore è opaco. Codifica le chiavi di ordinamento e i valori dell’ultima riga
per esse, quindi può solo continuare l’elenco da cui proviene: mantieni orderBy
identico tra le pagine, altrimenti la richiesta verrà rifiutata con
CURSOR_ORDER_MISMATCH invece di eseguire una ricerca in un ordine non richiesto. Una
richiesta che non specifica alcun orderBy adotta quello del cursore, permettendo di
passarlo direttamente senza dover ribadire l’ordinamento.
Non analizzarlo (parse) e non costruirne uno: la codifica esiste per essere modificata e
qualsiasi altra cosa produrrà INVALID_CURSOR.
Dalla natura del cursore derivano quattro conseguenze:
afternon può essere combinato conoffsetopage(400CURSOR_WITH_OFFSET). Entrambi indicano dove inizia la pagina, e rispettarli entrambi comporterebbe il salto di alcune righe.- Gli ordinamenti a più chiavi e le chiavi nullable funzionano entrambi. Il confronto viene costruito su
ciascuna chiave nell’ordine specificato, con il posizionamento di NULL dichiarato
dall’ordinamento — non un singolo
>su una colonna. - La rilevanza non può essere un cursore. Un valore
_scoreviene calcolato per query e non viene memorizzato da nessuna parte, e due query con stringhe di ricerca diverse producono punteggi che non sono sulla stessa scala. Un tale elenco semplicemente non include unnextCursor; paginalo conoffset. Lo stesso vale per la distanza di una ricerca vettoriale, e un cursore inviato con una di esse viene rifiutato conVECTOR_CURSOR_UNSUPPORTED. - Una sottoscrizione non può proseguire da un cursore.
listen()riesegue la sua query a ogni scrittura, eafterindica un punto in una sola di queste esecuzioni, quindilisten({ after })viene rifiutato conCURSOR_NOT_LIVEprima di inviare qualcosa. Usaoffsetopageper una finestra live, oppurefind({ after })per leggere la pagina una volta.
Su HTTP si tratta di un solo parametro:
GET /api/data/orders?orderBy=createdAt:desc&limit=100GET /api/data/orders?orderBy=createdAt:desc&limit=100&after=eyJrIjpbWyJ…Saltare il conteggio
Sezione intitolata “Saltare il conteggio”Da 0.25 meta.total è un COUNT(*) su ogni riga che la query trova, eseguito accanto alla pagina — e su una
tabella grande costa più della pagina stessa. Una lettura che si limita a procedere in avanti non ne ha
mai bisogno, perché meta.hasMore non ne dipende: passa count: "none" e la lettura chiede invece una
riga oltre la pagina.
const { data, meta } = await client.data.orders.find({ limit: 100, count: "none" });meta.hasMore; // exactSenza conteggio non c’è alcun totale da riportare: su HTTP meta.total è assente, e in-process corrisponde
alle righe fino alla fine della pagina (offset più la pagina). Su HTTP si usa ?count=none;
?count=exact è il valore predefinito, e qualsiasi altro valore è un 400 INVALID_COUNT. iterate() e
findAll() lo inviano da soli a ogni pagina, dato che uno scorrimento non riporta alcun totale.
Quali letture sono incapsulate e quali no
Sezione intitolata “Quali letture sono incapsulate e quali no”Due strutture e una regola: una finestra è incapsulata, una risposta completa no.
| Metodo | Restituisce | Perché |
|---|---|---|
find(), listen() |
{ data, meta } |
Una pagina. meta.total / meta.hasMore sono l’unico modo per sapere se c’è dell’altro |
findAll(), createMany(), updateMany() |
M[] |
Non resta nulla da segnalare: lo scorrimento è terminato, o il batch è l’insieme delle righe |
iterate() |
una riga alla volta | Nulla viene materializzato in memoria |
findById(), get(), create(), update() |
una riga | Non è una lista |
data non è un wrapper che l’SDK a volte aggiunge e a volte dimentica. È
il luogo in cui risiedono i metadati di paginazione, ed è presente esattamente quando ce ne sono.
Leggere tutto: iterate() e findAll()
Sezione intitolata “Leggere tutto: iterate() e findAll()”iterate() trasmette in streaming ogni riga corrispondente a una query, una alla volta, recuperando una pagina
alla volta dietro le quinte. Nulla si accumula, quindi è il metodo da utilizzare per una
collezione che non puoi mantenere in memoria:
for await (const order of client.data.orders.iterate({ where: { status: ["==", "pending"] }})) { await handleOrder(order);}findAll() è lo stesso scorrimento raccolto in un array:
const stale = await client.data.sessions.findAll({ where: { expiresAt: ["<", cutoff] }});Entrambi sono disponibili anche sul fluent builder, dove .limit() diventa la dimensione della pagina
piuttosto che un totale:
const rows = await client.data.orders .where("status", "==", "pending") .orderBy("createdAt", "asc") .limit(500) // rows per request .findAll();Quattro opzioni configurano lo scorrimento:
| Opzione | Predefinito | Cosa fa |
|---|---|---|
pageSize |
200 | Righe per richiesta. |
cursor |
— | Richiede la ricerca: genera un errore invece di ripiegare sull’offset. Vedi sotto. |
maxPages |
10 000 | Limite massimo di richieste, per evitare che un server che restituisce continuamente hasMore rimanga bloccato in un ciclo infinito. |
maxRows |
10 000 | Solo per findAll(). Il superamento del limite genera un errore invece di restituire un array troncato come se fosse la risposta completa. Passa Infinity per disattivarlo, oppure usa iterate(). |
Lo scorrimento cerca da solo. Dopo la prima pagina restituisce
il cursore del server — meta.nextCursor — come after, quindi ogni richiesta
chiede righe strettamente successive all’ultima visualizzata. Le scritture concorrenti precedenti a quella
riga non possono spostare la finestra, e il database non deve mai saltare righe per conteggio, ed è questo
che rendeva lenti i lunghi scorrimenti con offset: ogni pagina costava più della precedente.
Dove il server non emette alcun cursore — una ricerca ordinata per rilevanza, una ricerca vettoriale, una
lettura distinct, una proiezione fields che esclude un campo di ordinamento — lo scorrimento pagina
invece tramite offset, e una riga inserita o eliminata durante l’esecuzione dello scorrimento può
causarne il salto o la ripetizione. Passa cursor per rendere la ricerca un requisito anziché una
preferenza:
for await (const job of client.data.jobs.iterate({ cursor: "id" })) { /* … */ }cursor specifica la colonna in base alla quale ordinare quando la query non lo dichiara già, e trasforma
il ripiego sull’offset in un errore. Lo scorrimento non costruisce alcun confronto autonomamente, motivo
per cui funziona anche un ordinamento a più chiavi —
for await (const job of client.data.jobs.iterate({ cursor: "id", orderBy: [["priority", "desc"], ["createdAt", "asc"]]})) { /* … */ }— e per cui funziona anche una chiave di ordinamento nullable.
L’ordinamento deve comunque essere totale, il che in pratica significa univoco: l’ID di riga
risolve l’eventuale parità finale, quindi qualsiasi colonna funziona come criterio di spareggio. Dato
cursor, uno scorrimento il cui cursore smette di avanzare genera l’errore cursor-stalled invece di
andare in loop infinito, e una query che nessun cursore può descrivere (rilevanza) genera
cursor-missing; rimuovi cursor e lo scorrimento pagina queste tramite offset.
Vedi anche
Sezione intitolata “Vedi anche”- Interrogare i dati — filtri, fluent builder, ordinamento.
- Aggregazioni e ricerca — perché la rilevanza non può fungere da chiave per un cursore.