Imposer l'unicité sur plusieurs attributs dans DynamoDB
DynamoDB garantit l'unicité pour exactement une chose : la clé primaire. Il n'y a pas
de contrainte UNIQUE (email), pas de UNIQUE (username), et rien qui couvre deux
attributs. En venant de SQL, cette absence est la première surprise — et le premier
endroit où les gens livrent discrètement une race condition.
Comment imposer une contrainte d'unicité sur plusieurs attributs dans DynamoDB ?
DynamoDB n'a pas de contrainte UNIQUE au-delà de la clé primaire, tu dois donc imposer l'unicité toi-même : modélise chaque valeur protégée comme son propre item marqueur dont la clé est cette valeur, puis écris l'enregistrement et chaque marqueur ensemble dans un seul TransactWriteItems, chaque put gardé par attribute_not_exists. La collision que le moteur impose déjà devient ta contrainte.
- Il n'y a pas de contrainte d'unicité — seule la clé primaire est imposée unique par le moteur. Tout autre attribut « doit être unique » est ton boulot.
- Modélise chaque règle d'unicité comme son propre item. Un item marqueur dédié dont la clé est la valeur que tu protèges transforme « cet email est-il pris ? » en une collision de clé que le moteur impose déjà.
- Écris-les atomiquement avec
TransactWriteItems. Une transaction, chaque put gardé parattribute_not_exists, pour que tous les marqueurs et le vrai enregistrement commitent ensemble ou aucun. - Ne fais pas check-then-write. Une lecture-avant-insertion est une race manuelle ; deux signups concurrents lisent tous les deux « libre » et écrivent tous les deux.
Pourquoi l'approche évidente est fausse
L'instinct est de faire un Query (ou pire, un Scan) sur l'email, ne rien voir, puis
PutItem le nouveau compte. C'est une race check-then-act.
Deux personnes enregistrent ada@lovelace.io à la même milliseconde. Les deux lectures
reviennent vides. Les deux écritures réussissent. Tu as maintenant deux comptes sur un
email — et rien dans la table ne le signale.
Un GSI sur email ne te sauve pas non plus. Les GSI sont
à cohérence à terme, donc la lecture qui garde ton écriture
peut être périmée par conception. La solution n'est pas une vérification plus rapide ;
c'est de faire en sorte que l'écriture elle-même refuse d'atterrir sur une valeur prise.
Modéliser chaque contrainte comme un item marqueur
Le moteur impose déjà une règle d'unicité gratuitement : tu ne peux pas écrire deux items avec la même clé. Donc encode chaque règle d'unicité comme une clé.
À côté du vrai item de compte, écris un item marqueur par attribut protégé. La clé de partition du marqueur est la valeur namespacée. Si la valeur est prise, la clé existe, et un put gardé ne peut pas l'écraser.
Pour un signup qui doit garder à la fois email et username uniques, trois items se
déplacent ensemble — clés dans une disposition single-table (voir
single-table design) :
| Item | PK | SK | Objectif |
|---|---|---|---|
| Enregistrement de compte | ACCT#a1f9c3 | PROFILE | Le vrai compte |
| Verrou email | UNIQ#EMAIL#ada@lovelace.io | LOCK | Réserve l'email |
| Verrou username | UNIQ#HANDLE#ada | LOCK | Réserve le username |
La propre PK du compte est un id généré (ACCT#a1f9c3) — jamais l'email — pour que
l'utilisateur puisse changer son email plus tard sans réécrire la clé primaire. Les
items de verrou ne portent aucune donnée de profil ; ils existent seulement pour que
leur clé soit occupée.
Écrire les trois atomiquement
TransactWriteItems
applique jusqu'à 100 écritures comme une seule unité tout-ou-rien. Garde chaque put avec
attribute_not_exists(PK) pour qu'il échoue si cette clé est déjà présente.
Si une seule condition échoue — le verrou email, le verrou handle ou le compte lui-même
— DynamoDB rollback toute la transaction et lève une
TransactionCanceledException. Pas de signup partiel, pas de verrou orphelin.
{
"TransactItems": [
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "ACCT#a1f9c3"},
"SK": {"S": "PROFILE"},
"email": {"S": "ada@lovelace.io"},
"username": {"S": "ada"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
},
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "UNIQ#EMAIL#ada@lovelace.io"},
"SK": {"S": "LOCK"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
},
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "UNIQ#HANDLE#ada"},
"SK": {"S": "LOCK"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
}
]
}La condition est tout le mécanisme. Sans attribute_not_exists, un second signup avec
le même email écrase silencieusement le premier verrou. Avec, le put refuse, la
transaction s'annule, et ton appli fait remonter « email déjà utilisé ».
Construire la ConditionExpression et la map de valeurs à la main, c'est là que les
fautes de frappe se glissent. Le
DynamoDB Expression Builder émet la condition et
l'Item typé pour chaque put pour que tu puisses coller une transaction correcte
directement dans ton appel SDK.
Lis l'échec, ne le devine pas
Quand la transaction est annulée, DynamoDB renvoie un tableau CancellationReasons
positionnellement — une entrée par item, dans l'ordre de la requête. Un
ConditionalCheckFailed en slot 1 signifie que l'email est pris ; le slot 2 signifie
que le username l'est. Remappe le slot vers une erreur précise au niveau du champ
plutôt qu'un « échec du signup » générique.
Inspecter les verrous dans DynoTable
Les items marqueurs sont invisibles dans l'UI de ton appli — c'est de la plomberie. Quand un signup échoue mystérieusement, tu as besoin de voir si le verrou existe réellement.
Ouvre la table dans DynoTable et Query le préfixe UNIQ#. Le compte et ses deux
items de verrou se trouvent ensemble, donc un signup bloqué (un verrou laissé derrière
par une suppression ratée) est évident d'un coup d'œil.

Garder les verrous honnêtes au changement et à la suppression
Les verrous ne sont pas écrits une fois pour toutes. Ils reflètent la valeur en direct, donc le cycle de vie doit les garder synchronisés — chaque opération qui touche un attribut protégé est aussi une transaction.
- Changer l'email. Une transaction : pose le nouveau verrou
UNIQ#EMAIL#…avecattribute_not_exists, supprime l'ancien verrou, mets à jour le compte. Même garantie tout-ou-rien. - Supprimer le compte. Supprime l'item de compte et les deux items de verrou en une transaction, sinon tu échoueras un verrou qui bloque la valeur à jamais.
- Réessayer en sûreté. Passe un
ClientRequestTokenpour qu'une transaction renvoyée (après un blip réseau) soit idempotente plutôt qu'une double écriture.
Le piège est de traiter le verrou comme du fire-and-forget. Un verrou créé au signup mais jamais supprimé à la suppression du compte est une valeur que personne ne peut jamais réutiliser — et ça ne se montrera pas avant qu'un vrai utilisateur ne puisse pas réclamer son propre ancien handle.
Étapes suivantes
Les marqueurs d'unicité sont un pattern single-table, donc ils se trouvent naturellement
à côté de tes autres items — lis single-table design pour
la disposition des clés, et Query vs Scan pour ne jamais
atteindre un Scan afin de vérifier un verrou. Le pattern a d'abord été déroulé dans la
session AWS re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions.
Rédige les puts gardés par condition avec le DynamoDB Expression Builder, puis essaie DynoTable pour inspecter les items de verrou sur ta propre table.


