Fortgeschritten5 Min. Lesezeit

DynamoDB Condition Expressions

Eine Condition Expression ist ein Prädikat, das DynamoDB vor dem Commit deines Writes auf dem bestehenden Item auswertet. Ist das Prädikat falsch, wird der Write abgelehnt und nichts ändert sich. Es ist das Nächste, was DynamoDB an eine WHERE-Klausel auf einem Write hat — und der einzige sichere Weg, eine Invariante zu erzwingen.

Wie funktionieren DynamoDB Condition Expressions?

Eine Condition Expression ist ein Prädikat, das DynamoDB serverseitig gegen das aktuelle Item auswertet, bevor ein Write committet wird. Ist es wahr, läuft der Write durch; ist es falsch, wird der Write mit ConditionalCheckFailedException abgelehnt und nichts ändert sich. Sie faltet die Prüfung und die Mutation zu einer einzigen atomaren Operation, sodass gleichzeitige Aufrufer nicht gegen einen veralteten Read racen können.

  • Es ist ein Guard, kein Filter. ConditionExpression läuft serverseitig auf dem aktuellen Item; ein falsches Ergebnis lässt den Write mit ConditionalCheckFailedException scheitern.
  • Es ersetzt Read-then-Write. Kein SELECT-dann-UPDATE-Round-Trip — Prüfung und Mutation sind eine atomare Operation, sodass zwei Aufrufer nicht racen können.
  • Es ist gratis abzulehnen, nicht gratis auszuführen. Ein gescheiterter Conditional Write verbraucht trotzdem Write Capacity. Die Garantie kostet dasselbe wie der Write, den sie blockiert.

Aus SQL kommend würdest du die Zeile lesen, sie im App-Code prüfen, dann aktualisieren. In DynamoDB ist diese Lücke zwischen Read und Write ein Datenkorruptions-Bug, der auf einen gleichzeitigen Aufrufer wartet. Die Condition Expression schließt die Lücke.

Wo sie gelten

Du hängst eine ConditionExpression an PutItem, UpdateItem, DeleteItem und jede Action innerhalb von TransactWriteItems. Sie ist kein Teil von Query oder Scan — die verwenden FilterExpression, was auf dem Read-Pfad etwas anderes ist.

Diese Unterscheidung bringt Leute durcheinander, also sei präzise:

ConditionExpressionFilterExpression
PfadWrites (Put/Update/Delete)Reads (Query/Scan)
Effekt bei FehlschlagLehnt den ganzen Write abLässt das Item aus den Ergebnissen fallen
SiehtDas aktuelle Item, pre-WriteJedes Kandidaten-Item, post-Read
KostenGescheiterter Write rechnet trotzdem abGefilterte Items werden für den Read trotzdem abgerechnet

Beide laufen serverseitig. Der Unterschied ist, was "falsch" bewirkt: eine Bedingung bricht eine Mutation ab; ein Filter versteckt nur eine Zeile, für deren Read du schon bezahlt hast. (AWS: Condition Expressions)

Die Funktionen, die du tatsächlich nutzt

Die Bedingungssprache ist klein. Die Arbeitspferde:

  • attribute_exists(path) / attribute_not_exists(path) — existiert dieses Attribut auf dem Item? Das klassische Idiom für "nur anlegen, wenn abwesend" / "nur aktualisieren, wenn vorhanden".
  • Vergleichsoperatoren — =, <>, <, <=, >, >= — gegen einen Wert oder ein anderes Attribut.
  • attribute_type, begins_with, contains, size — Typ- und String/Set-Checks.
  • BETWEEN … AND …, IN (…) — Range und Mitgliedschaft.
  • AND, OR, NOT, Klammern — um das Obige zu kombinieren.

attribute_not_exists auf dem Partition Key ist der kanonische Weg, PutItem sich wie ein Insert verhalten zu lassen, der ein bestehendes Item nicht überschreibt — DynamoDB hat keine separate "Insert"-Operation, also ist die Bedingung die Insert-Semantik. (AWS: Comparison Operator and Function Reference)

Ein durchgespieltes Beispiel: ein Ledger gegen Überziehung absichern

Nimm ein Bank-Ledger. Jedes Konto ist ein Item:

PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents    = 0

Die Invariante: eine Abbuchung darf den verfügbaren Saldo nie unter null drücken, und du darfst nie ein Konto belasten, das nicht existiert. Zwei Regeln, beide im Write selbst erzwingbar.

Der falsche Weg (die Falle)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← app-seitige Prüfung
UpdateItem  SET clearedCents = 20000

Zwischen dem GetItem und dem UpdateItem kann eine zweite Abbuchung dieselben 50000 lesen, ihre eigene Prüfung bestehen und ebenfalls schreiben. Beide gelingen; das Konto geht ins Minus. Das ist ein Read-modify-write-Race, und kein Maß an app-seitiger Validierung behebt es — Prüfung und Write sind getrennte Operationen.

Der richtige Weg

Falte die Prüfung in den Write. Buche 30000 Cent ab, bedingt darauf, dass das Konto existiert und genug hält:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = clearedCents - :amt
  ConditionExpression:
    attribute_exists(PK) AND clearedCents >= :amt

mit :amt = 30000. Wenn der Saldo zu niedrig ist oder das Item nie angelegt wurde, lehnt DynamoDB den Write mit ConditionalCheckFailedException ab und der Saldo bleibt unangetastet. Die gleichzeitige Abbuchung sieht entweder den ursprünglichen Saldo und wird dagegen geprüft, oder sie sieht den aktualisierten — nie einen veralteten Read, auf dem sie handelte.

Du kannst die exakte Expression — Namen, Werte und alles — mit dem DynamoDB Expression Builder bauen und kopieren, statt die ExpressionAttributeValues-Map von Hand zusammenzubauen.

Den Guard in DynoTable inspizieren

Wenn ein Conditional Write scheitert, willst du den echten Zustand des Items sehen, nicht raten. Hol das Konto-Item hoch und lies clearedCents direkt.

Die Ledger-Collection in DynoTable — das BALANCE-Item zeigt clearedCents oberhalb der Transaktions-Items des Kontos.
Die Ledger-Collection in DynoTable — das BALANCE-Item zeigt clearedCents oberhalb der Transaktions-Items des Kontos.

Lies die Ablehnung, retry nicht blind

ConditionalCheckFailedException ist kein transienter Fehler — denselben Write zu wiederholen ändert nichts. Es bedeutet, dass eine Geschäftsregel feuerte: unzureichende Mittel, doppeltes Anlegen, veraltete Version. Stelle es als Domänen-Ergebnis dar, nicht als Infra-Aussetzer.

Zwei Dinge machen Fehlschläge debuggbar:

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB gibt das aktuelle Item neben dem Fehlschlag zurück, sodass du "Saldo war 20000, du hast 30000 verlangt" ohne einen zweiten Read zeigen kannst. (AWS: Working with Items)
  • Die beiden Fehlschlagsgründe unterscheiden. attribute_exists(PK) AND clearedCents >= :amt kollabiert "kein Konto" und "keine Mittel" in eine Exception. Wenn Aufrufer sie auseinanderhalten müssen, splitte in zwei Writes oder inspiziere das zurückgegebene Item.

Optimistic Locking ist derselbe Trick

Das Versionsnummern-Muster ist nur eine Condition Expression mit einem anderen Hut. Speichere ein version-Attribut; jeder Write behauptet die Version, die du gelesen hast, und erhöht sie:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = :new, version = :next
  ConditionExpression: version = :seen

Wenn ein anderer Writer zuerst zog, ist version = :seen falsch, der Write wird abgelehnt, und du liest erneut und versuchst es nochmal. So macht DynamoDB Nebenläufigkeitskontrolle ohne Locks — behaupte, was du sahst, scheitere, wenn es sich bewegte. (AWS: Optimistic Locking with Version Number)

Fallstricke und nächste Schritte

  • Namen, die mit reservierten Wörtern kollidieren. status, size, name und ~570 weitere sind reserviert. Aliasiere sie mit ExpressionAttributeNames (#s = status), oder deine Expression scheitert still beim Parsen.
  • Eine Bedingung kann kein anderes Item referenzieren. Sie sieht nur das Item, das geschrieben wird. Item-übergreifende Invarianten brauchen TransactWriteItems mit einer ConditionExpression pro Action oder einen ConditionCheck gegen ein Sentinel-Item.
  • Gescheiterte Writes kosten trotzdem WCUs. Ein Guard, der 90 % der Zeit ablehnt, rechnet diese Ablehnungen trotzdem ab. Billige Versicherung, aber nicht gratis.

Zum Modellieren der Keys, gegen die diese Guards laufen, siehe Single-Table Design und Query vs. Scan. Wenn du bereit bist, Conditional Writes gegen echte Daten auszugeben, lade DynoTable herunter und führe sie gegen deine eigenen Tabellen aus.

Aktualisiert