Fortgeschritten5 Min. Lesezeit

DynamoDB Expression Attribute Names und Values

DynamoDB-Expressions sind Vorlagen: Du schreibst Platzhalter und lieferst die echten und -werte in zwei Begleit-Maps nach. #name ist ein Namensplatzhalter, :value ein Wertplatzhalter. Verwechselst du die beiden, lehnt DynamoDB den ganzen Aufruf ab.

Was ist der Unterschied zwischen #name und :value in DynamoDB?

#name ist ein Platzhalter für einen Attributnamen, geliefert über ExpressionAttributeNames; :value ist ein Platzhalter für einen Attributwert, geliefert über ExpressionAttributeValues. Nimm #name, um reservierten Wörtern, Punkten oder Leerzeichen auszuweichen, und :value für jedes Literal — DynamoDB setzt Werte nie inline ein. Austauschbar sind sie nicht; vertauschst du sie, gibt es eine ValidationException.

  • #name ersetzt einen Attributnamen über ExpressionAttributeNames — nimm es immer dann, wenn ein Attribut mit einem reservierten Wort kollidiert oder einen Punkt bzw. ein Leerzeichen enthält.
  • :value ersetzt einen Wert über ExpressionAttributeValues — DynamoDB setzt nie Literale in den Expression-Text ein, jeder Wert ist also ein Platzhalter.
  • Austauschbar sind sie nicht. Ein #, wo ein : hingehört, ist eine ValidationException, kein stilles No-op.

Aus SQL kommend setzt du beides inline — WHERE status = 'published'. DynamoDB setzt keins von beidem inline. Genau diese Trennung bringt jeden Neuling zum Stolpern.

Warum es zwei Maps gibt

In SQL trägt der Query-String alles: Spaltennamen, Literale, Operatoren. DynamoDB trennt die Form der Expression bewusst von ihren Daten.

Werte bekommen eine eigene Map, damit DynamoDB jeden einzeln typisieren kann (S, N, BOOL, …) und damit der Parser nie raten muss, wo ein String endet — es gibt kein Quoting und kein Escaping, das schiefgehen könnte. Die vollständige Typ-Tag-Liste steht unter Datentypen in DynamoDB.

Namen bekommen dieselbe Behandlung, aus einem anderen Grund: DynamoDB hat eine lange Liste reservierter Wörter, und jedes Attribut, das eines davon trifft, darf in einer Expression nicht als nackter Name auftauchen. Der Platzhalter umgeht die Reservierung komplett.

Die Falle der reservierten Wörter

Hier eine CMS-Artikeltabelle — Partitionsschlüssel BLOG#<blog>, Sortierschlüssel ARTICLE#<slug> —, deren Attribute sich natürlich lesen, aber zufällig mit reservierten Wörtern kollidieren:

AttributReserviert?Was es enthält
statusjadraft / published
namejaAnzeigename des Autors
sizejagerenderte Bytelänge
ttljaArchivablauf (Epoch)
slugneinURL-Slug

status, name, size und ttl stehen alle auf der Liste reservierter Wörter von AWS, dieser Filter scheitert also schon am ersten Wort:

FilterExpression  status = :s

DynamoDB antwortet mit einer ValidationException"Attribute name is a reserved keyword; reserved keyword: status". Die Lösung ist ein Namensplatzhalter, niemals ein Umbenennen des Attributs:

FilterExpression           #status = :s
ExpressionAttributeNames   { "#status": "status" }
ExpressionAttributeValues  { ":s": { "S": "published" } }

slug ist nicht reserviert, eine gegen slug getestete Abfrage funktioniert also, und du nimmst an, die nächste tut es auch. Dann bricht status sie. Die Liste ändert sich, also lern sie nicht auswendig — setz jeden Namen als Platzhalter, dann erwischt es dich nie.

Jeden Wert abbilden, immer

Bei Werten gibt es nichts zu verhandeln: Es existiert keine Syntax für ein Inline-Literal. Selbst eine schlichte Zahl bekommt einen Platzhalter. Dieses Update markiert einen Artikel als veröffentlicht, hält seine Größe fest und setzt eine 30-Tage-Archiv-:

UpdateExpression:          SET #status = :s, #size = :sz, #ttl = :exp
ExpressionAttributeNames:  { "#status": "status", "#size": "size", "#ttl": "ttl" }
ExpressionAttributeValues: {
  ":s":   { "S": "published" },
  ":sz":  { "N": "20480" },
  ":exp": { "N": "1719792000" }
}

Beachte: :sz und :exp gehen als N-Strings raus — der Zahlentyp von DynamoDB wird auf der Leitung als String kodiert. Die Wert-Map ist auch der Ort, an dem du einen Wert über Klauseln hinweg wiederverwendest: :s einmal definieren, in ConditionExpression und FilterExpression referenzieren.

Diese beiden Maps von Hand zu bauen ist genau die Stelle, an der sich Tippfehler verstecken. Der Expression Builder erzeugt den Expression-String und beide Maps gemeinsam, mit ausgefüllten Typ-Tags, damit die Platzhalter nicht auseinanderlaufen können.

Der Builder unten filtert auf status — ein reserviertes Wort —, du kannst also zusehen, wie er in der ExpressionAttributeNames-Map automatisch auf #status aliast:

Erstelle deine Anfrage
Generierter Code
new QueryCommand({
  "TableName": "AuditLog",
  "KeyConditionExpression": "#hashKey = :hashKeyValue",
  "FilterExpression": "#filter0 = :filterValue0",
  "ExpressionAttributeNames": {
    "#hashKey": "pk",
    "#filter0": "status"
  },
  "ExpressionAttributeValues": {
    ":hashKeyValue": {
      "S": "TENANT#acme"
    },
    ":filterValue0": {
      "S": "active"
    }
  }
})

Namen für verschachtelte und sperrige Pfade

Der #-Platzhalter kann mehr, als reservierten Wörtern auszuweichen. Die Dokumentpfad-Syntax nutzt Punkte und Klammern, ein Attribut, das buchstäblich einen Punkt enthält — etwa ein Metadaten-Schlüssel og.title — ist ohne Platzhalter also gar nicht adressierbar:

ProjectionExpression       #og
ExpressionAttributeNames   { "#og": "og.title" }

Ohne ihn liest DynamoDB og.title als „das Feld title innerhalb der Map og" — etwas völlig anderes. Dasselbe gilt für Namen mit Leerzeichen oder führenden Ziffern. Bei Verschachtelung setzt du jedes Segment als Platzhalter: #meta.#author, mit #meta und #author beide definiert.

Namen vs. Werte, nebeneinander

#name:value
Ersetzteinen Attributnameneinen Attributwert
MapExpressionAttributeNamesExpressionAttributeValues
Präfix#:
Nötig fürreservierte Wörter, Punkte, Leerzeichenimmer — keine Inline-Literale
Falscher Typ ergibtValidationExceptionValidationException

Wäre ein Wert als Name getippt, würde DynamoDB nach einem Attribut namens published suchen, und deine Bedingung träfe nie so zu, wie du es gemeint hast — deshalb scheitert die API laut. Diese Strenge ist ein Feature: Es gibt keine stille falsche Antwort.

Fallstricke und nächste Schritte

  • Einen Platzhalter deklarieren, den du nicht benutzt — DynamoDB lehnt ungenutzte Einträge in beiden Maps ab. Bau die Maps aus der Expression heraus, nicht im Voraus.
  • :v nach dem Umbauen der Expression weiterverwenden — lässt du eine Klausel weg, kann ihr Wert übrig bleiben und den Ungenutzt-Fehler auslösen. Der Builder hält beides im Gleichschritt.
  • Annehmen, ein Name sei sicher, weil er einmal funktioniert hat — Kollisionen mit reservierten Wörtern gelten pro Attribut. Setz einheitlich Platzhalter und hör auf zu raten.

Diese Maps tauchen in jedem Schreibpfad auf, sie passen also natürlich zu Single-Table Design und dazu, zu wissen, wann Query und wann Scan, bevor du überhaupt einen Filter anhängst.

Lass dir die Expression samt beider Maps vom Expression Builder erzeugen und probier DynoTable aus, um sie gegen deine eigenen Tabellen laufen zu lassen und den Platzhaltern beim Auflösen zuzusehen.

Aktualisiert