Intermédiaire6 min de lecture

Les condition expressions DynamoDB

Une condition expression est un prédicat que DynamoDB évalue sur l'item existant avant de valider ton écriture. Si le prédicat est faux, l'écriture est rejetée et rien ne change. C'est ce que DynamoDB a de plus proche d'une clause WHERE sur une écriture — et le seul moyen sûr d'imposer un invariant.

Comment fonctionnent les condition expressions DynamoDB ?

Une condition expression est un prédicat que DynamoDB évalue côté serveur sur l'item courant avant de valider une écriture. S'il est vrai, l'écriture est effectuée ; s'il est faux, l'écriture est rejetée avec ConditionalCheckFailedException et rien ne change. Elle fond la vérification et la mutation en une seule opération atomique, de sorte que des appelants concurrents ne peuvent pas se courir après sur une lecture périmée.

  • C'est un garde, pas un filtre. ConditionExpression s'exécute côté serveur sur l'item courant ; un résultat faux fait échouer l'écriture avec ConditionalCheckFailedException.
  • Elle remplace le read-then-write. Pas d'aller-retour SELECT puis UPDATE — la vérification et la mutation sont une seule opération atomique, donc deux appelants ne peuvent pas se courir après.
  • Le rejet est gratuit, l'exécution non. Une écriture conditionnelle échouée consomme quand même de la capacité d'écriture. La garantie coûte autant que l'écriture qu'elle bloque.

Venant de SQL, tu lirais la ligne, la vérifierais dans le code applicatif, puis mettrais à jour. Dans DynamoDB cet écart entre lecture et écriture est un bug de corruption de données qui attend un appelant concurrent. La condition expression ferme l'écart.

Où elles s'appliquent

Tu attaches une ConditionExpression à PutItem, UpdateItem, DeleteItem, et à chaque action dans un TransactWriteItems. Elle ne fait pas partie de Query ni de Scan — ceux-là utilisent FilterExpression, qui est une autre chose sur le chemin de lecture.

Cette distinction fait trébucher les gens, alors sois précis :

ConditionExpressionFilterExpression
CheminÉcritures (Put/Update/Delete)Lectures (Query/Scan)
Effet en cas d'échecRejette toute l'écritureRetire l'item des résultats
VoitL'item courant, avant écritureChaque item candidat, après lecture
CoûtL'écriture échouée facture quand mêmeLes items filtrés sont quand même facturés à la lecture

Les deux s'exécutent côté serveur. La différence est ce que fait « faux » : une condition avorte une mutation ; un filtre masque juste une ligne que tu as déjà payée pour lire. (AWS : Condition Expressions)

Les fonctions que tu utiliseras vraiment

Le langage de condition est petit. Les bêtes de somme :

  • attribute_exists(path) / attribute_not_exists(path) — cet attribut existe-t-il sur l'item ? L'idiome classique pour « créer seulement si absent » / « mettre à jour seulement si présent ».
  • Comparateurs — =, <>, <, <=, >, >= — contre une valeur ou un autre attribut.
  • attribute_type, begins_with, contains, size — vérifications de type et de chaîne/ensemble.
  • BETWEEN … AND …, IN (…) — plage et appartenance.
  • AND, OR, NOT, parenthèses — pour combiner ce qui précède.

attribute_not_exists sur la clé de partition est le moyen canonique de faire en sorte que PutItem se comporte comme un insert qui n'écrasera pas un item existant — DynamoDB n'a pas d'opération « insert » séparée, donc la condition est la sémantique d'insert. (AWS : Comparison Operator and Function Reference)

Un exemple travaillé : protéger un grand livre contre le découvert

Prends un grand livre bancaire. Chaque compte est un item :

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

L'invariant : un débit ne doit jamais pousser le solde disponible sous zéro, et tu ne dois jamais débiter un compte qui n'existe pas. Deux règles, toutes deux imposables dans l'écriture elle-même.

La mauvaise façon (l'arme à fragmentation)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← vérification côté app
UpdateItem  SET clearedCents = 20000

Entre le GetItem et le UpdateItem, un second débit peut lire le même 50000, passer sa propre vérification, et écrire aussi. Les deux réussissent ; le compte passe négatif. C'est une course read-modify-write, et aucune validation côté app ne la corrige — la vérification et l'écriture sont des opérations séparées.

La bonne façon

Replie la vérification dans l'écriture. Débite 30000 cents, conditionnel à l'existence du compte et au fait qu'il en détienne assez :

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

avec :amt = 30000. Si le solde est trop bas, ou si l'item n'a jamais été créé, DynamoDB rejette l'écriture avec ConditionalCheckFailedException et le solde est intact. Le débit concurrent soit voit le solde original et est vérifié contre lui, soit voit le mis à jour — jamais une lecture périmée sur laquelle il a agi.

Tu peux construire et copier l'expression exacte — noms, valeurs, et tout — avec le DynamoDB expression builder au lieu d'assembler à la main la map ExpressionAttributeValues.

Inspecter le garde dans DynoTable

Quand une écriture conditionnelle échoue, tu veux voir l'état réel de l'item, pas le deviner. Ouvre l'item du compte et lis clearedCents directement.

La collection du grand livre dans DynoTable — l'item BALANCE affiche clearedCents au-dessus des items de transaction du compte.
La collection du grand livre dans DynoTable — l'item BALANCE affiche clearedCents au-dessus des items de transaction du compte.

Lis le rejet, ne réessaie pas aveuglément

ConditionalCheckFailedException n'est pas une erreur transitoire — réessayer la même écriture ne change rien. Ça signifie qu'une règle métier s'est déclenchée : fonds insuffisants, création en double, version périmée. Présente-le comme un résultat métier, pas comme un hoquet d'infra.

Deux choses rendent les échecs débogables :

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB renvoie l'item courant à côté de l'échec, pour que tu puisses montrer « le solde était 20000, tu as demandé 30000 » sans une seconde lecture. (AWS : Working with Items)
  • Distinguer les deux raisons d'échec. attribute_exists(PK) AND clearedCents >= :amt fond « pas de compte » et « pas de fonds » en une seule exception. Si les appelants doivent les distinguer, sépare en deux écritures ou inspecte l'item renvoyé.

Le verrouillage optimiste est la même astuce

Le motif du numéro de version n'est qu'une condition expression portant un autre chapeau. Stocke un attribut version ; chaque écriture affirme la version que tu as lue et l'incrémente :

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

Si un autre écrivain a bougé en premier, version = :seen est faux, l'écriture est rejetée, et tu relis et réessaies. C'est ainsi que DynamoDB fait du contrôle de concurrence sans verrous — affirme ce que tu as vu, échoue si ça a bougé. (AWS : Optimistic Locking with Version Number)

Pièges et étapes suivantes

  • Des noms qui entrent en collision avec des mots réservés. status, size, name, et ~570 autres sont réservés. Aliase-les avec ExpressionAttributeNames (#s = status) ou ton expression échoue silencieusement à se parser.
  • Une condition ne peut pas référencer un autre item. Elle ne voit que l'item en cours d'écriture. Les invariants inter-items nécessitent un TransactWriteItems avec une ConditionExpression par action, ou un ConditionCheck contre un item sentinelle.
  • Les écritures échouées coûtent quand même des WCU. Un garde qui rejette 90 % du temps facture quand même ces rejets. Assurance bon marché, mais pas gratuite.

Pour modéliser les clés contre lesquelles ces gardes s'exécutent, vois single-table design et Query vs Scan. Quand tu es prêt à émettre des écritures conditionnelles contre de vraies données, télécharge DynoTable et lance-les contre tes propres tables.

Mis à jour