Intermedio6 min di lettura

Condition expression in DynamoDB

Una condition expression è un predicato che DynamoDB valuta sull'item esistente prima di committare la tua scrittura. Se il predicato è falso, la scrittura è rifiutata e nulla cambia. È la cosa più vicina a una clausola WHERE su una scrittura che DynamoDB possiede — e l'unico modo sicuro per imporre un'invariante.

Come funzionano le condition expression in DynamoDB?

Una condition expression è un predicato che DynamoDB valuta lato server sull'item corrente prima di committare una scrittura. Se è vero, la scrittura procede; se è falso, la scrittura viene rifiutata con ConditionalCheckFailedException e nulla cambia. Fonde il controllo e la mutazione in un'unica operazione atomica, così i chiamanti concorrenti non possono fare una gara su una lettura obsoleta.

  • È una guardia, non un filtro. La ConditionExpression viene eseguita lato server sull'item corrente; un risultato falso fa fallire la scrittura con ConditionalCheckFailedException.
  • Sostituisce il read-then-write. Niente round trip SELECT poi UPDATE — il controllo e la mutazione sono un'unica operazione atomica, così due chiamanti non possono fare una gara.
  • È gratis rifiutare, non gratis eseguire. Una scrittura condizionale fallita consuma comunque capacità di scrittura. La garanzia costa quanto la scrittura che blocca.

Venendo da SQL, leggeresti la riga, la controlleresti nel codice dell'app, poi la aggiorneresti. In DynamoDB quel divario tra lettura e scrittura è un bug di corruzione dati in attesa di un chiamante concorrente. La condition expression chiude il divario.

Dove si applicano

Attacchi una ConditionExpression a PutItem, UpdateItem, DeleteItem e a ogni azione dentro TransactWriteItems. Non fa parte di Query o Scan — quelli usano FilterExpression, che è una cosa diversa sul percorso di lettura.

Quella distinzione confonde le persone, quindi sii preciso:

ConditionExpressionFilterExpression
PercorsoScritture (Put/Update/Delete)Letture (Query/Scan)
Effetto al fallimentoRifiuta l'intera scritturaToglie l'item dai risultati
VedeL'item corrente, pre-scritturaOgni item candidato, post-lettura
CostoLa scrittura fallita fattura comunqueGli item filtrati sono comunque fatturati nella lettura

Entrambe girano lato server. La differenza è cosa fa "falso": una condizione aborta una mutazione; un filtro nasconde solo una riga che hai già pagato per leggere. (AWS: Condition Expressions)

Le funzioni che userai davvero

Il linguaggio delle condizioni è piccolo. I cavalli da tiro:

  • attribute_exists(path) / attribute_not_exists(path) — questo attributo esiste sull'item? L'idioma classico per "crea solo se assente" / "aggiorna solo se presente".
  • Comparatori — =, <>, <, <=, >, >= — contro un valore o un altro attributo.
  • attribute_type, begins_with, contains, size — controlli di tipo e di stringa/set.
  • BETWEEN … AND …, IN (…) — intervallo e appartenenza.
  • AND, OR, NOT, parentesi — per combinare i precedenti.

attribute_not_exists sulla partition key è il modo canonico per far comportare PutItem come un insert che non sovrascrive un item esistente — DynamoDB non ha un'operazione "insert" separata, quindi la condizione è la semantica dell'insert. (AWS: Comparison Operator and Function Reference)

Un esempio pratico: proteggere un registro contabile dallo scoperto

Prendi un registro contabile bancario. Ogni conto è un item:

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

L'invariante: un addebito non deve mai spingere il saldo disponibile sotto zero, e non devi mai addebitare un conto che non esiste. Due regole, entrambe imponibili nella scrittura stessa.

Il modo sbagliato (il footgun)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← controllo lato app
UpdateItem  SET clearedCents = 20000

Tra il GetItem e l'UpdateItem, un secondo addebito può leggere lo stesso 50000, superare il proprio controllo e scrivere anch'esso. Entrambi vanno a buon fine; il conto va in negativo. È una gara read-modify-write, e nessuna quantità di validazione lato app la risolve — il controllo e la scrittura sono operazioni separate.

Il modo giusto

Incorpora il controllo nella scrittura. Addebita 30000 centesimi, condizionato al conto esistente e che ne abbia abbastanza:

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

con :amt = 30000. Se il saldo è troppo basso, o l'item non è mai stato creato, DynamoDB rifiuta la scrittura con ConditionalCheckFailedException e il saldo resta intatto. L'addebito concorrente o vede il saldo originale ed è controllato contro di esso, o vede quello aggiornato — mai una lettura obsoleta su cui ha agito.

Puoi costruire e copiare l'espressione esatta — nomi, valori e tutto — con il DynamoDB expression builder invece di assemblare a mano la mappa ExpressionAttributeValues.

Ispezionare la guardia in DynoTable

Quando una scrittura condizionale fallisce, vuoi vedere lo stato reale dell'item, non indovinarlo. Apri l'item del conto e leggi clearedCents direttamente.

La collezione del registro contabile in DynoTable — l'item BALANCE mostra clearedCents sopra gli item di transazione del conto.
La collezione del registro contabile in DynoTable — l'item BALANCE mostra clearedCents sopra gli item di transazione del conto.

Leggi il rifiuto, non ritentare alla cieca

ConditionalCheckFailedException non è un errore transitorio — ritentare la stessa scrittura non cambia nulla. Significa che una regola di business è scattata: fondi insufficienti, creazione duplicata, versione obsoleta. Esponilo come esito di dominio, non come un singhiozzo dell'infrastruttura.

Due cose rendono i fallimenti debuggabili:

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB restituisce l'item corrente insieme al fallimento, così puoi mostrare "il saldo era 20000, ne hai chiesti 30000" senza una seconda lettura. (AWS: Working with Items)
  • Distinguere le due ragioni di fallimento. attribute_exists(PK) AND clearedCents >= :amt collassa "nessun conto" e "nessun fondo" in un'unica eccezione. Se i chiamanti devono distinguerle, dividi in due scritture o ispeziona l'item restituito.

L'optimistic locking è lo stesso trucco

Il pattern del numero di versione è solo una condition expression con un altro cappello. Memorizza un attributo version; ogni scrittura asserisce la versione che hai letto e la incrementa:

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

Se un altro writer si è mosso prima, version = :seen è falso, la scrittura è rifiutata, e rileggi e ritenti. È così che DynamoDB fa il controllo di concorrenza senza lock — asserisci ciò che hai visto, fallisci se si è mosso. (AWS: Optimistic Locking with Version Number)

Trappole e prossimi passi

  • Nomi che si scontrano con parole riservate. status, size, name e ~570 altre sono riservate. Aliasale con ExpressionAttributeNames (#s = status) o la tua espressione fallisce silenziosamente nel parsing.
  • Una condizione non può riferirsi a un altro item. Vede solo l'item che si sta scrivendo. Le invarianti tra item richiedono TransactWriteItems con una ConditionExpression per azione, o un ConditionCheck contro un item sentinella.
  • Le scritture fallite costano comunque WCU. Una guardia che rifiuta il 90% delle volte fattura comunque quei rifiuti. Assicurazione economica, ma non gratis.

Per modellare le chiavi contro cui girano queste guardie, vedi single-table design e Query vs Scan. Quando sei pronto a emettere scritture condizionali contro dati reali, scarica DynoTable ed eseguile contro le tue tabelle.

Aggiornato