Intermédiaire7 min de lecture

Condition Expressions DynamoDB : le guide complet (avec exemples)

Une expression de condition 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 qui se rapproche le plus, dans DynamoDB, d'une clause WHERE sur une écriture — et la seule façon sûre d'imposer un invariant.

Comment fonctionnent les expressions de condition DynamoDB ?

Une expression de condition 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 se poursuit ; 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 courir sur une lecture périmée.

  • C'est un garde-fou, 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 lire-puis-écrire. Pas d'aller-retour SELECT puis UPDATE — la vérification et la mutation forment une seule opération atomique, de sorte que deux appelants ne peuvent pas se concurrencer.
  • Rejeter est gratuit, s'exécuter ne l'est pas. Une écriture conditionnelle échouée consomme quand même de la capacité d'écriture. Une écriture rejetée facture des WCU pour la taille de l'item existant sur lequel elle a été vérifiée (minimum 1) — un create-if-absent échoué coûte 1 WCU.

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

Où elles s'appliquent

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

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

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

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

Les fonctions que tu utiliseras vraiment

Le langage de condition est réduit. Les incontournables :

  • attribute_exists(path) / attribute_not_exists(path) — cet existe-t-il sur l'item ? L'idiome classique pour « créer seulement si absent » / « mettre à jour seulement si présent ».
  • Comparateurs — =, <>, <, <=, >, >= — face à 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 les précédents.

attribute_not_exists sur la est la manière canonique de faire que PutItem se comporte comme un insert qui n'écrase pas un item existant — DynamoDB n'a pas d'opération « insert » distincte, donc la condition constitue la sémantique d'insert. (AWS : Référence des opérateurs de comparaison et des fonctions)

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 faire passer 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 (le piège)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← app-side check
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 lui aussi. Les deux réussissent ; le compte passe en négatif. C'est une course lire-modifier-écrire, et aucune validation côté application ne la corrige — la vérification et l'écriture sont des opérations distinctes.

La bonne façon

Fonds la vérification dans l'écriture. Débite 30000 centimes, à condition que le compte existe et 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 faible, ou si l'item n'a jamais été créé, DynamoDB rejette l'écriture avec ConditionalCheckFailedException et le solde reste intact. Le débit concurrent voit soit le solde d'origine et est vérifié par rapport à lui, soit le solde 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 générateur d'expressions DynamoDB au lieu d'assembler à la main la map ExpressionAttributeValues.

Essaie-le ici même — ce générateur est préréglé sur un PutItem protégé (attribute_not_exists) pour que tu puisses lire la ConditionExpression générée :

Construis ta requête
Code généré
new PutItemCommand({
  "TableName": "AuditLog",
  "Item": {
    "pk": {
      "S": "TENANT#acme"
    },
    "sk": {
      "S": "EVENT#2026-06-24T10:00:00Z"
    },
    "action": {
      "S": "login"
    }
  },
  "ConditionExpression": "attribute_not_exists(#cond0)",
  "ExpressionAttributeNames": {
    "#cond0": "pk"
  }
})

Inspecter le garde-fou dans DynoTable

Quand une écriture conditionnelle échoue, tu veux voir l'état réel de l'item, pas le deviner. Fais remonter 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. Cela signifie qu'une règle métier s'est déclenchée : fonds insuffisants, création en double, version périmée. Fais-en un résultat métier, pas un incident d'infrastructure.

Deux choses rendent les échecs débogables :

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB renvoie l'item courant en même temps que l'échec, pour que tu puisses afficher « le solde était de 20000, tu as demandé 30000 » sans une seconde lecture. (AWS : Utilisation des items)
  • Distinguer les deux raisons d'échec. attribute_exists(PK) AND clearedCents >= :amt réunit « pas de compte » et « pas de fonds » en une seule exception. Si les appelants ont besoin de les distinguer, découpe 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 expression de condition 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 rédacteur a bougé en premier, version = :seen est faux, l'écriture est rejetée, et tu relis et réessaies. C'est ainsi que DynamoDB gère le contrôle de concurrence sans verrous — affirme ce que tu as vu, échoue si ça a bougé. (AWS : Verrouillage optimiste avec numéro de version) La zone de staging de DynoTable applique ce pattern pour toi — une modification concurrente apparaît comme un conflit à résoudre, pas comme une écriture perdue.

Pièges et étapes suivantes

  • Des noms qui entrent en collision avec des mots réservés. status, size, name et environ 570 autres sont réservés. Aliase-les avec ExpressionAttributeNames (#s = status) ou la requête est rejetée avec une ValidationException (« Attribute name is a reserved keyword »). Le vérificateur de mots réservés prend tes noms d'attributs et te rend la map d'alias prête à coller.
  • 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 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-fou qui rejette 90 % du temps facture quand même ces rejets. Assurance bon marché, mais pas gratuite.

Pour modéliser les clés sur lesquelles ces garde-fous s'exécutent, voir la conception à table unique et Query vs Scan. Quand tu es prêt à émettre des écritures conditionnelles sur des données réelles, télécharge DynoTable et exécute-les sur tes propres tables.

Mis à jour