Paginierung
Drei Möglichkeiten, eine Collection zu durchlaufen: ein Offset, eine Seitenzahl und ein Cursor. Die ersten beiden sind positionsbasiert und die dritte nicht, was den entscheidenden Unterschied ausmacht – eine positionsbasierte Seite liest erneut, indem sie vom Anfang an zählt, sodass Zeilen, die während des Paginierens geschrieben werden, die Bedeutung von „Zeile 20“ verschieben.
// 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 });limit muss eine ganze Zahl zwischen 1 und 1000 sein. Ein größerer Wert – oder eine Null, ein negativer
Wert oder ein Bruch – wird mit 400 INVALID_LIMIT abgelehnt, anstatt begrenzt zu werden, da eine
stillschweigend kleinere Seite nicht von der letzten Seite unterschieden werden kann. Um über diese
Obergrenze hinaus zu lesen, durchlaufen Sie die Seiten mit iterate() oder findAll().
Cursor-Paginierung
Abschnitt betitelt „Cursor-Paginierung“Jede Listenantwort enthält ein meta.nextCursor, solange eine weitere Seite vorhanden ist.
Übergeben Sie diesen als after zurück, und die nächste Seite setzt strikt nach der zuletzt ausgelieferten Zeile
an, anstatt bei einer Zeilenanzahl, die durch gleichzeitige Schreibvorgänge bereits verschoben wurde:
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);Der Cursor ist opak. Er kodiert die Sortierschlüssel und die Werte der letzten Zeile für diese
Schlüssel, sodass er nur die Auflistung fortsetzen kann, aus der er stammt: Behalten Sie orderBy über
alle Seiten hinweg identisch bei, andernfalls wird die Anfrage mit CURSOR_ORDER_MISMATCH abgelehnt,
anstatt in einer Reihenfolge zu suchen, die niemand angefordert hat. Eine Anfrage, die überhaupt kein
orderBy angibt, übernimmt die Sortierung des Cursors, sodass Sie ihn direkt zurückgeben können, ohne
die Sortierung erneut anzugeben.
Parsen Sie ihn nicht und erstellen Sie keinen eigenen: Die Kodierung existiert, um geändert werden zu
können, und alles andere führt zu INVALID_CURSOR.
Aus der Funktionsweise eines Cursors ergeben sich drei Dinge:
afterkann nicht mitoffsetoderpagekombiniert werden (400CURSOR_WITH_OFFSET). Beide geben an, wo die Seite beginnt, und die Berücksichtigung beider würde Zeilen überspringen.- Sortierungen über mehrere Schlüssel und null-fähige Schlüssel funktionieren beide. Der Vergleich
wird über jeden Schlüssel der Reihe nach aufgebaut, mit der NULL-Platzierung,
die die Sortierung deklariert hat – nicht ein einzelnes
>auf einer Spalte. - Relevanz kann kein Cursor sein. Ein
_scorewird pro Abfrage berechnet und nirgendwo gespeichert, und zwei Abfragen mit unterschiedlichen Suchbegriffen erzeugen Scores, die nicht auf derselben Skala liegen. Eine solche Auflistung enthält schlicht keinnextCursor; paginieren Sie diese mitoffset.
Über HTTP ist es ein Parameter:
GET /api/data/orders?orderBy=createdAt:desc&limit=100GET /api/data/orders?orderBy=createdAt:desc&limit=100&after=eyJrIjpbWyJ…Welche Leseoperationen gewrappt sind und welche nicht
Abschnitt betitelt „Welche Leseoperationen gewrappt sind und welche nicht“Zwei Formen und eine Regel: Ein Ausschnitt ist gewrappt, eine vollständige Antwort nicht.
| Methode | Rückgabewert | Warum |
|---|---|---|
find(), listen() |
{ data, meta } |
Eine Seite. meta.total / meta.hasMore sind die einzige Möglichkeit zu wissen, ob noch mehr vorhanden ist |
findAll(), createMany(), updateMany() |
M[] |
Nichts übrig, was berichtet werden müsste – der Durchlauf ist beendet oder der Batch sind die Zeilen |
iterate() |
eine Zeile nach der anderen | Es wird überhaupt nichts materialisiert |
findById(), get(), create(), update() |
eine Zeile | Keine Liste |
data ist kein Wrapper, den das SDK manchmal hinzufügt und manchmal vergisst. Hier befinden sich die
Paginierungs-Metadaten, und es ist genau dann vorhanden, wenn es welche gibt.
Alles lesen: iterate() und findAll()
Abschnitt betitelt „Alles lesen: iterate() und findAll()“iterate() streamt jede Zeile, die zu einer Abfrage passt, nacheinander und ruft im Hintergrund
seitenweise Daten ab. Es sammelt sich nichts an, daher ist dies die Methode der Wahl für eine Collection,
die Sie nicht vollständig im Speicher halten können:
for await (const order of client.data.orders.iterate({ where: { status: ["==", "pending"] }})) { await handleOrder(order);}findAll() ist derselbe Durchlauf, gesammelt in einem Array:
const stale = await client.data.sessions.findAll({ where: { expiresAt: ["<", cutoff] }});Beide sind auch auf dem Fluent Builder verfügbar, wo .limit() zur Seitengröße anstelle einer
Gesamtzahl wird:
const rows = await client.data.orders .where("status", "==", "pending") .orderBy("createdAt", "asc") .limit(500) // rows per request .findAll();Drei Optionen steuern den Durchlauf:
| Option | Standard | Was sie bewirkt |
|---|---|---|
pageSize |
200 | Zeilen pro Anfrage. |
cursor |
— | Sucht über eine Spalte, anstatt per Offset zu paginieren. Siehe unten. |
maxPages |
10 000 | Obergrenze für Anfragen, damit ein Server, der nie aufhört, hasMore zu melden, nicht endlos läuft. |
maxRows |
10 000 | Nur findAll(). Ein Überschreiten wirft einen Fehler, anstatt ein abgeschnittenes Array zurückzugeben, als wäre es das vollständige Ergebnis. Übergeben Sie Infinity, um dies zu deaktivieren, oder verwenden Sie iterate(). |
Bevorzugen Sie cursor, wann immer die Collection eine eindeutige, sortierbare Spalte hat.
Offset-Paginierung zählt Zeilen bei jeder Anfrage neu, sodass eine Zeile, die während des Durchlaufs
eingefügt oder gelöscht wird, das Fenster verschiebt und der Durchlauf stillschweigend Zeilen überspringt
oder wiederholt. Beim Seeking werden Zeilen strikt nach der zuletzt gesehenen angefordert, was durch
gleichzeitige Schreibvorgänge vor dem Cursor nicht verschoben werden kann:
for await (const job of client.data.jobs.iterate({ cursor: "id" })) { /* … */ }cursor bedeutet hier „Seek statt Paginierung per Offset“ und gibt die Spalte an, nach der sortiert
werden soll, wenn die Abfrage dies nicht bereits definiert. Das Seeking selbst ist
der Cursor des Servers: Der Durchlauf gibt meta.nextCursor als after zurück
und baut keinen eigenen Vergleich auf, weshalb auch eine Sortierung über mehrere Schlüssel funktioniert –
for await (const job of client.data.jobs.iterate({ cursor: "id", orderBy: [["priority", "desc"], ["createdAt", "asc"]]})) { /* … */ }— und warum auch ein null-fähiger Sortierschlüssel funktioniert.
Die Sortierung muss dennoch total sein, was in der Praxis eindeutig bedeutet: Die Zeilen-ID
löst den finalen Gleichstand auf, sodass jede Spalte als Tie-Breaker funktioniert. Ein Durchlauf, dessen
Cursor nicht mehr voranschreitet, wirft jedoch cursor-stalled, anstatt in einer Endlosschleife zu
verharren. Eine Abfrage, die kein Cursor beschreiben kann (Relevanz), wirft cursor-missing; entfernen
Sie cursor und paginieren Sie per Offset.
Siehe auch
Abschnitt betitelt „Siehe auch“- Daten abfragen – Filter, der Fluent Builder, Sortierung.
- Aggregate & Suche – Warum Relevanz keinen Cursor bestimmen kann.