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 });Verwenden Sie das eine oder das andere. Seit 0.25 page und offset zusammen beschreiben dasselbe Fenster
doppelt, und der Read wird mit PAGE_WITH_OFFSET abgelehnt, anstatt stillschweigend
page anzuwenden. Das SDK lehnt es vor jeder Anfrage ab, über HTTP, In-Process und
offline; der Server antwortet auf eine Roh-Anfrage, die beides sendet, mit einem 400.
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);nextCursor fehlt auf der letzten Seite, und auf einer Seite, die kein Cursor beschreiben kann:
einer nach Relevanz sortierten Suche (_score), einer Vektorsuche und einer fields-Projektion,
die eines der orderBy-Felder auslässt — der Cursor wird aus den Sortierwerten der letzten Zeile
gebaut, und diese Zeile trägt sie dann nicht. Dort kann meta.hasMore true sein, ohne dass es
ein nextCursor gibt, und die obige Schleife stoppt vorzeitig: Fügen Sie die Sortierfelder zu
fields hinzu, oder paginieren Sie per offset. iterate() mit einem cursor weist eine solche
Auflistung mit cursor-missing ab, statt einfach zu stoppen. Eine Sortierung nach einem
Aggregat über eine Relation paginiert dagegen per Cursor.
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 vier 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. Mit der Distanz einer Vektorsuche verhält es sich genauso, und ein Cursor, der mit einer gesendet wird, wird mitVECTOR_CURSOR_UNSUPPORTEDabgelehnt. - Ein Abonnement kann nicht an einem Cursor fortsetzen.
listen()führt seine Abfrage bei jedem Schreibvorgang erneut aus, undafterbezeichnet eine Stelle in einem Durchlauf davon, daher wirdlisten({ after })mitCURSOR_NOT_LIVEabgelehnt, bevor etwas gesendet wird. Verwenden Sieoffsetoderpagefür ein Live-Fenster, oderfind({ after }), um die Seite einmal zu lesen.
Ü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…Die Zählung überspringen
Abschnitt betitelt „Die Zählung überspringen“Seit 0.25 meta.total ist ein COUNT(*) über jede Zeile, die die Abfrage trifft, neben der Seite
ausgeführt — und bei einer großen Tabelle kostet es mehr als die Seite selbst. Ein Read, der nur
vorwärts paginiert, braucht es nie, denn meta.hasMore hängt nicht davon ab: Übergeben Sie
count: "none", und der Read fragt stattdessen eine Zeile über die Seite hinaus ab.
const { data, meta } = await client.data.orders.find({ limit: 100, count: "none" });meta.hasMore; // exaktOhne Zählung gibt es keine Gesamtzahl zu melden: Über HTTP fehlt meta.total, und im Prozess
sind es die Zeilen bis zum Ende der Seite (offset plus die Seite). Über HTTP lautet es
?count=none; ?count=exact ist der Standard, und jeder andere Wert ist ein 400
INVALID_COUNT. iterate() und findAll() senden es auf jeder Seite von selbst, da ein
Durchlauf keine Gesamtzahl meldet.
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();Vier Optionen steuern den Durchlauf:
| Option | Standard | Was sie bewirkt |
|---|---|---|
pageSize |
200 | Zeilen pro Anfrage. |
cursor |
— | Seeking verlangen: einen Fehler werfen, statt auf Offset zurückzufallen. 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(). |
Der Durchlauf arbeitet von selbst per Seek. Nach der ersten Seite gibt er
den Cursor des Servers — meta.nextCursor — als after zurück, sodass
jede Anfrage Zeilen strikt nach der zuletzt gesehenen anfordert. Gleichzeitige Schreibvorgänge
vor dieser Zeile können das Fenster nicht verschieben, und die Datenbank muss nie Zeilen nach
Anzahl überspringen, was lange Offset-Durchläufe langsam gemacht hat: Jede Seite kostete mehr als
die vorherige.
Wo der Server keinen Cursor ausstellt — eine nach Relevanz sortierte Suche, eine Vektorsuche, ein
distinct-Read, eine fields-Projektion, die ein Sortierfeld auslässt —, paginiert der Durchlauf
stattdessen per Offset, und eine Zeile, die während des Durchlaufs eingefügt oder gelöscht wird,
kann dazu führen, dass er eine Zeile überspringt oder wiederholt. Übergeben Sie cursor, um das
Seeking von einer Präferenz zu einer Anforderung zu machen:
for await (const job of client.data.jobs.iterate({ cursor: "id" })) { /* … */ }cursor gibt die Spalte an, nach der sortiert werden soll, wenn die Abfrage dies nicht bereits
definiert, und macht den Offset-Fallback zu einem Fehler. Der Durchlauf 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. Mit cursor wirft ein
Durchlauf, dessen Cursor nicht mehr voranschreitet, cursor-stalled, anstatt in einer Endlosschleife
zu verharren, und eine Abfrage, die kein Cursor beschreiben kann (Relevanz), wirft cursor-missing;
entfernen Sie cursor, und der Durchlauf paginiert diese per Offset.
Siehe auch
Abschnitt betitelt „Siehe auch“- Daten abfragen – Filter, der Fluent Builder, Sortierung.
- Aggregate & Suche – Warum Relevanz keinen Cursor bestimmen kann.