Table-API-Referenz¶
Überblick¶
Table ist die Basisklasse aller Modelle. Sie liefert das typisierte CRUD, das Abfragesystem, das Laden der Beziehungen und den Lebenszyklus einer Instanz.
Was sie bietet:
- Strikte Typisierung, aus der Klasse selbst abgeleitet
- CRUD als statische Methoden und als Instanzmethoden
- Ein Abfragesystem, das
GetItem,BatchGetItem,QueryoderScananhand der Form des Filters wählt - Beziehungen
HasMany,HasOne,BelongsToundManyToManymit Batch-Laden - Automatische Zeitstempel und Soft Delete
- Cursor-Paginierung, Sortierung, Projektion und verschachtelte Includes
Import¶
Modelldefinition¶
import {
Table, Name, PrimaryKey, NotNull, Default, Index,
CreatedAt, UpdatedAt, DeleteAt, CreationOptional
} from '@arcaelas/dynamite';
@Name("users")
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Index()
@NotNull()
declare email: string;
@NotNull()
declare name: string;
@Default(() => 25)
declare age: CreationOptional<number>;
@CreatedAt()
declare created_at: CreationOptional<string>;
@UpdatedAt()
declare updated_at: CreationOptional<string>;
@DeleteAt()
declare deleted_at: CreationOptional<string>;
}
Konstruktor¶
constructor(data: Partial<InferAttributes<T>>)¶
Erzeugt eine Instanz im Speicher. Es wird nichts nach DynamoDB geschrieben.
Verhalten:
- Führt die Schreib-Pipeline aller Spalten aus, nicht nur der in
datavorhandenen. Deshalb sind@Default,@PrimaryKeyund@CreatedAtschon beim Erzeugen gesetzt, und deshalb weist@NotNullein fehlendes Feld genau dort zurück - Setzt einen konfigurierten Client voraus: ohne
connect()wirft der Konstruktor - Die Instanz ist erst nach
save()odercreate()persistiert
const user = new User({ email: "john@example.com", name: "John Doe" });
user.id; // "01JBQ8..." — bereits erzeugt
user.created_at; // bereits erzeugt
await user.save(); // jetzt existiert der Datensatz in DynamoDB
Mutations-Optionen¶
Jede Mutation nimmt dasselbe Optionsobjekt als letztes Argument:
interface MutationOptions {
hook?: boolean; // führt die Lifecycle-Hooks aus; standardmäßig aus
tx?: TransactionContext; // führt innerhalb einer atomaren Transaktion aus
}
- Hooks sind pro Aufruf opt-in: ohne
{ hook: true }läuft keiner. - In einer Transaktion wird nichts geschrieben, bis der Callback zurückkehrt. Die
after*-Hooks und__isPersistedgreifen erst nach dem Commit. increment()unddecrement()akzeptieren{ tx }, lösen aber nie Hooks aus.
Instanzmethoden¶
save(options?: MutationOptions): Promise<boolean>¶
Schreibt das komplette Item.
Verhalten:
- Bei einer nie persistierten Instanz delegiert sie an
create(), das einen vorhandenen Primärschlüssel nicht überschreibt - Bei einer persistierten Instanz sendet sie ein
PutItemmit allen Spalten, sodass jedes von Hand geänderte Feld geschrieben wird - Ob eine Instanz persistiert ist, wird intern geführt und nicht aus der id abgeleitet: eine neue Instanz hat ihre id durch
@PrimaryKeybereits gesetzt
Rückgabe: true
const user = new User({ email: "jane@example.com", name: "Jane Smith" });
await user.save(); // Anlage
user.name = "Jane Doe";
await user.save(); // vollständiges Neuschreiben des Items
update(patch: Partial<InferAttributes<T>>, options?: MutationOptions): Promise<boolean>¶
Aktualisiert nur die übergebenen Felder.
Verhalten:
- Beziehungsfelder in
patchwerden ignoriert statt zu werfen - Die
@UpdatedAt-Spalten werden erneuert, auch wenn sie nicht inpatchstehen - Ohne Hooks und außerhalb einer Transaktion ist es ein einziges
UpdateItem, das nur die berührten Felder schreibt, unter der Bedingung, dass der Datensatz noch existiert. Existiert er nicht mehr, wirdfalsezurückgegeben und nichts geschrieben - Mit
{ hook: true }werden die Änderungen angewendet,beforeUpdateausgeführt, das Item geschrieben undafterUpdateausgeführt. Beide Hooks erhalten das Änderungs-Delta
Rückgabe: true, wenn der Datensatz aktualisiert wurde
destroy(options?: MutationOptions): Promise<null>¶
Löscht den Datensatz, sanft wenn das Modell es zulässt.
Verhalten:
- Mit einer
@DeleteAt-Spalte wird dort der aktuelle Zeitstempel geschrieben und gespeichert: der Datensatz bleibt in der Tabelle und verschwindet auswhere() - Ohne
@DeleteAtwird der Datensatz entfernt - Wirft
Cannot destroy record without ID, wenn die Instanz keinen Primärschlüssel hat
await post.destroy(); // Soft Delete
await post.destroy({ hook: true }); // beforeDestroy + afterDestroy
forceDestroy(options?: MutationOptions): Promise<null>¶
Entfernt den Datensatz mit einem DeleteItem und ignoriert @DeleteAt.
increment(feld, menge = 1): Promise<void> / decrement(feld, menge = 1): Promise<void>¶
Addiert oder subtrahiert atomar auf dem Server, ohne den vorherigen Wert zu lesen, und spiegelt die Änderung im Speicher.
- Der Typ beschränkt
feldauf die numerischen Spalten des Modells - Wirft
Cannot increment without primary key, wenn die Instanz keine id hat
attach<R>(Modell, related_id, pivot_data?): Promise<void>¶
Fügt eine Zeile in die Pivot-Tabelle einer @ManyToMany-Beziehung ein.
- Die Instanz muss persistiert sein, sonst wirft die Methode
- Sie ist idempotent, ein vorhandenes Paar bleibt unverändert
pivot_dataergänzt zusätzliche Spalten in der Pivot-Zeile- Die Suche läuft über den GSI
<fremdschlüssel>_indexder Pivot-Tabelle, nie über einen Scan
detach<R>(Modell, related_id): Promise<void>¶
Entfernt die Pivot-Zeile dieses Paares. Fehlt die Beziehung, die Zeile oder der lokale Schlüssel, passiert nichts.
sync<R>(Modell, related_ids): Promise<void>¶
Lässt die Beziehung genau related_ids enthalten: entfernt, was nicht auf der Liste steht, und ergänzt, was fehlt, in Blöcken von 25.
- Wirft, wenn das verwandte Modell kein Schema hat, wenn es keine
@ManyToMany-Beziehung zwischen beiden Modellen gibt, oder wenn der lokale Schlüssel undefiniert ist
toJSON(): Record<string, unknown>¶
Einfaches Objekt mit den Spalten des Modells. Lässt null und undefined aus und serialisiert geladene Beziehungen rekursiv.
toString(): string¶
JSON.stringify der Instanz.
Statische Methoden¶
create<M>(data, options?: MutationOptions): Promise<M>¶
Legt einen Datensatz an.
- Schreibt mit
attribute_not_existsauf den Primärschlüssel: überschreibt nie und wirftRecord with <schlüssel> '<wert>' already exists in <tabelle>, wenn die id vergeben ist - In einer Transaktion gilt die Instanz erst nach dem Commit als persistiert
const user = await User.create({ name: "Juan", email: "juan@example.com" });
await User.create({ name: "Juan" }, { hook: true });
await dynamite.tx(async (tx) => { await User.create({ name: "Juan" }, { tx }); });
createMany<M>(zeilen, options?: MutationOptions): Promise<M[]>¶
Legt mehrere Datensätze mit BatchWriteItem an, 25 pro Anfrage, und wiederholt, was DynamoDB unverarbeitet zurückgibt.
- Doppelte Primärschlüssel lassen sich nicht prüfen,
BatchWriteItemkennt diese Bedingung nicht: ein vorhandener Datensatz wird überschrieben - Gibt die bereits als persistiert markierten Instanzen zurück
const logs = await Log.createMany([
{ level: "info", message: "Start" },
{ level: "warn", message: "Cache leer" }
]);
update<M>(änderungen, filter, options?: MutationOptions): Promise<number>¶
Aktualisiert alle passenden Datensätze und gibt deren Anzahl zurück.
- Bei einem einfachen Filter auf den Primärschlüssel ist es ein einziges
UpdateItemmit den berührten Feldern und ohne vorheriges Lesen, sofern kein@Setund kein@Validatedieser Felder das Argumentcurrentdeklariert. Tut es das, wird der Datensatz zuerst gelesen, um ihn übergeben zu können - Bei jedem anderen Filter wird die Abfrage aufgelöst, die Änderungen werden angewendet und in Blöcken von 25 geschrieben
- Die
@UpdatedAt-Spalten werden bei jedem betroffenen Datensatz erneuert - Mit
{ hook: true }laufenbeforeUpdateundafterUpdateeinmal pro betroffenem Datensatz
const betroffen = await User.update({ status: "suspended" }, { status: "inactive" });
await User.update({ status: "active" }, { id: "user-1" });
delete<M>(filter, options?: MutationOptions): Promise<number>¶
Löscht alle passenden Datensätze und gibt deren Anzahl zurück.
- Immer ein endgültiges Löschen, mit oder ohne
@DeleteAt: Soft Delete ist eine Entscheidung der Instanz und lebt indestroy() - Ein einfacher Filter auf den Primärschlüssel bei einem Modell ohne
@DeleteAtund ohne Destroy-Hooks ist ein einzigesDeleteItem - Sonst wird die Abfrage aufgelöst und in Blöcken von 25 gelöscht
const gelöscht = await User.delete({ status: "suspended" });
await User.delete({ id: "user-1" }, { hook: true });
deleteMany<M>(ids, options?: MutationOptions): Promise<number>¶
Löscht über den Primärschlüssel mit BatchWriteItem, ohne vorher zu lesen. Immer endgültig und ohne Hooks.
increment<M>(feld, menge, filter, options?): Promise<number> / decrement<M>(...)¶
Atomare Addition auf dem Server.
- Ein Filter auf den Primärschlüssel aktualisiert genau diesen Datensatz, ohne ihn zu lesen
- Jeder andere Filter löst zuerst die Abfrage auf und aktualisiert dann alle Treffer parallel
- Gibt zurück, wie viele Datensätze berührt wurden
await User.increment("credits", 10, { id: "user-1" });
await User.decrement("stock", 1, { sku: "ABC" });
first<M>(filter, options?): Promise<M | undefined>¶
Der erste passende Datensatz oder undefined. Es ist where() mit limit: 1, auf einem indizierten Feld also eine einzige Anfrage.
const user = await User.first({ email: "juan@example.com" });
const neueste = await User.first({ role: "admin" }, { order: { created_at: "DESC" } });
last<M>(filter?, options?): Promise<M | undefined>¶
Der letzte Datensatz, absteigend sortiert nach der @CreatedAt-Spalte oder, falls es keine gibt, nach dem Primärschlüssel.
Ohne Sort Key auf der Tabelle wird im Speicher sortiert, was bedeutet, alles zu lesen, was zum Filter passt, um einen Datensatz zu behalten. Auf einer großen Tabelle nimmt man first(filter, { order: { created_at: "DESC" } }), eingegrenzt durch einen @Index.
where() — Abfragen¶
Überladungen¶
User.where(filter, optionen?)
User.where(feld, wert, optionen?)
User.where(feld, operator, wert, optionen?)
await User.where({ status: "active" });
await User.where("name", "Juan");
await User.where("age", ">=", 18);
await User.where({ age: { $gte: 18, $lte: 65 } });
Operatoren¶
| Operator | Aliase | Bedeutung |
|---|---|---|
= | $eq | Gleich. Mit null: "das Attribut existiert nicht" |
<>, != | $ne | Ungleich. Mit null: "das Attribut existiert" |
< | $lt | Kleiner als |
<= | $lte | Kleiner oder gleich |
> | $gt | Größer als |
>= | $gte | Größer oder gleich |
in | $in | Im Array enthalten |
include | $include, contains, $contains | Enthält den Teilstring oder das Element |
Eine unbekannte Spalte wirft Unknown column '<feld>' in <tabelle>. Ein leeres Array bei in wirft Operator 'in' requires a non-empty array.
Optionen¶
const users = await User.where({ status: "active" }, {
order: { created_at: "DESC" }, // nach Feld; "ASC"/"DESC" allein sortiert nach @CreatedAt
limit: 10,
skip: 20, // Alias: offset
cursor: vorherige.cursor, // nächste Seite; ignoriert skip
attributes: ["id", "name"], // Projektion
deleted: true, // schließt die per Soft Delete markierten ein
include: {
profile: true,
orders: { where: { status: "completed" }, limit: 5 }
}
});
limit: 0gibt ein leeres Array zurück, ohne das Netz zu berühren.orderallein sortiert nach der@CreatedAt-Spalte, sonst nach dem Primärschlüssel. Um nach einem Datum zu sortieren, muss man es benennen:{ created_at: "DESC" }.attributeserzeugt Instanzen mit ausschließlich diesen Spalten.deletedersetzt das frühere_includeTrashed, das als Alias weiterhin funktioniert.
Ergebnis und Paginierung¶
where() gibt das Array der Instanzen mit einer nicht aufzählbaren Eigenschaft cursor zurück. Sie trägt einen Wert, solange weitere Seiten existieren.
let seite = await User.where({}, { limit: 50 });
while (seite.cursor) {
seite = await User.where({}, { limit: 50, cursor: seite.cursor });
}
skip liest und verwirft auf jeder Seite alles Vorherige; der Cursor liest nur die angeforderte Seite.
Kosten und Leistung¶
| Filter | Kommando | Anfragen |
|---|---|---|
= auf den Primärschlüssel | GetItem | 1 |
in auf den Primärschlüssel | BatchGetItem | 1 je 100 Schlüssel |
= oder in auf eine @Index-Spalte | Query auf <feld>_index | 1 pro unterschiedlichem Wert |
| Jeder andere Filter | Scan | die ganze Tabelle, serverseitig gefiltert |
- Zusätzliche Filter über einer Lesung per Primärschlüssel werden auf dem bereits gelesenen Item ausgewertet: die Abfrage bleibt eine einzige Anfrage.
- Ein
limitbricht die Lesung ab, sobald genug Datensätze zusammen sind, und reist alsLimitmit, wenn serverseitig nichts mehr zu filtern ist. - Eine Lesung ohne
limit, die in einemScanendet, wird in vier parallele Segmente geteilt: dieselben Leseeinheiten, ein Bruchteil der Latenz. Ohneorderist die Reihenfolge beliebig, wie schon zuvor. - Existiert der GSI eines
@Indexnicht, schlägt die Abfrage nicht fehl: sie fällt aufScanzurück, entfernt den Index aus ihrer internen Registrierung und läuft weiter. Es funktioniert und kostet die ganze Tabelle — deklariere<feld>_indexmit ProjektionALLin deiner Infrastruktur. attributesverkleinert die Nutzlast, nicht die Leseeinheiten: DynamoDB berechnet das gesamte Item.- Beziehungen werden im Batch geladen: eine Runde Abfragen pro Beziehung und Ebene, bis zu fünf Ebenen. Pivot-Tabellen werden über ihren GSI
<fremdschlüssel>_indexgelesen.
Fehler¶
| Meldung | Ursache |
|---|---|
DynamoDB client no configurado. Use Dynamite.connect() primero. | Eine Instanz wurde erzeugt oder eine Abfrage vor connect() ausgeführt |
Record with <schlüssel> '<wert>' already exists in <tabelle> | create() auf einen vergebenen Primärschlüssel |
Unknown column '<feld>' in <tabelle> | Ein Filter auf eine Spalte, die das Modell nicht deklariert |
Operator 'in' requires a non-empty array. | in mit leerem Array |
Cannot destroy record without ID | destroy()/forceDestroy() auf einer Instanz ohne Primärschlüssel |
Cannot increment without primary key | increment()/decrement() auf einer Instanz ohne Primärschlüssel |
No se puede attach sin ID: la instancia debe persistirse primero con save() o create() | attach() auf einer nie persistierten Instanz |
Transaction exceeds 100 operations limit | Mehr als 100 Operationen in einem tx() |
Grenzen¶
- Eine Transaktion fasst höchstens 100 Operationen und wird in Blöcken von 25 gesendet.
includeverschachtelt bis zu fünf Ebenen.BatchGetItemliest 100 Schlüssel pro Anfrage,BatchWriteItemschreibt 25; die Bibliothek teilt und wiederholt selbst.- DynamoDB begrenzt ein Item auf 400 KB und eine Abfrageseite auf 1 MB.
Quelldatei¶
src/core/table.ts