Unicité sur plusieurs attributs dans DynamoDB
DynamoDB garantit l'unicité pour exactement une chose : la . Il n'existe
aucune contrainte UNIQUE (email), aucune UNIQUE (username), et rien qui couvre deux
attributs. Quand tu viens de SQL, cette absence est la première surprise — et le premier
endroit où l'on livre discrètement une situation de compétition.
Comment imposer une contrainte d'unicité sur plusieurs attributs dans DynamoDB ?
DynamoDB n'a aucune contrainte UNIQUE au-delà de la , tu imposes donc 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 protégé par attribute_not_exists. La collision que le moteur impose déjà devient ta contrainte.
- Il n'existe aucune contrainte d'unicité — seule la clé primaire est imposée unique par le moteur. Tout autre attribut « qui doit être unique » est ton travail.
- 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 e-mail est-il pris ? » en une collision de clé que le moteur impose déjà.
- Écris-les de façon atomique avec
TransactWriteItems. Une seule , chaque put protégé parattribute_not_exists, pour que tous les marqueurs et l'enregistrement réel soient validés ensemble ou pas du tout. - Ne fais pas vérifier-puis-écrire. Une lecture-avant-insertion est une situation de compétition d'école ; deux inscriptions concurrentes lisent toutes deux « libre » et écrivent toutes deux.
Pourquoi l'approche évidente est mauvaise
L'instinct est de faire un Query (ou pire, un Scan) sur l'e-mail, de ne rien voir, puis un
PutItem du nouveau compte. C'est une situation de compétition vérifier-puis-agir.
Deux personnes inscrivent ada@lovelace.io à la même milliseconde. Les deux lectures reviennent
vides. Les deux écritures réussissent. Tu as maintenant deux comptes sur un seul e-mail — et rien
dans la table ne le signale.
Un sur email ne te sauve pas non plus. Les GSI sont en
cohérence à terme, si bien que la lecture qui conditionne 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élise chaque contrainte comme un item marqueur
Le moteur impose déjà gratuitement une règle d'unicité : tu ne peux pas écrire deux items avec la même clé. Encode donc chaque règle d'unicité comme une clé.
À côté de l'item de compte réel, écris un item marqueur par attribut protégé. La clé de partition du marqueur est la valeur préfixée par un espace de noms. Si la valeur est prise, la clé existe, et un put protégé ne peut pas l'écraser.
Pour une inscription qui doit garder à la fois email et username uniques, trois items se
déplacent ensemble — clés dans une disposition à table unique (voir
conception à table unique) :
| Item | PK | SK | Rôle |
|---|---|---|---|
| Enregistrement de compte | ACCT#a1f9c3 | PROFILE | Le compte réel |
| Verrou e-mail | UNIQ#EMAIL#ada@lovelace.io | LOCK | Réserve l'e-mail |
| Verrou nom d'utilisateur | UNIQ#HANDLE#ada | LOCK | Réserve le nom d'utilisateur |
La PK propre au compte est un id généré (ACCT#a1f9c3) — jamais l'e-mail — pour que
l'utilisateur puisse changer d'e-mail plus tard sans réécrire la clé primaire. Les items de verrou
ne portent aucune donnée de profil ; ils existent uniquement pour que leur clé soit occupée.
Écris les trois de façon atomique
TransactWriteItems
applique jusqu'à 100 écritures comme une seule unité tout-ou-rien. Protège 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 e-mail, le verrou de nom d'utilisateur ou le compte
lui-même — DynamoDB annule toute la transaction et lève TransactionCanceledException. Aucune
inscription partielle, aucun 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, une seconde inscription avec le
même e-mail écrase silencieusement le premier verrou. Avec elle, le put refuse, la transaction
s'annule, et ton application fait remonter « e-mail déjà utilisé ».
Construire à la main le ConditionExpression et la table de valeurs, c'est là que les fautes de
frappe se glissent. Le Générateur d'expressions DynamoDB
émet la condition et l'Item typé pour chaque put, afin que tu puisses coller une transaction
correcte directement dans ton appel de SDK.
Lis l'échec, ne le devine pas
Lorsque la transaction est annulée, DynamoDB renvoie un tableau CancellationReasons de manière
positionnelle — une entrée par item, dans l'ordre de la requête. Un ConditionalCheckFailed en
position 1 signifie que l'e-mail est pris ; la position 2 signifie que c'est le nom d'utilisateur.
Relie la position à une erreur précise, au niveau du champ, plutôt qu'à un générique « échec de
l'inscription ».
Inspecte les verrous dans DynoTable
Les items marqueurs sont invisibles dans l'interface de ton application — ils relèvent de la plomberie. Quand une inscription échoue mystérieusement, tu dois voir si le verrou existe réellement.
Ouvre la table dans DynoTable et fais un Query sur le préfixe UNIQ#. Le compte et ses deux
items de verrou sont côte à côte, si bien qu'une inscription bloquée (un verrou laissé derrière par
une suppression ratée) saute aux yeux.

Garde les verrous fidèles à la modification et à la suppression
Les verrous ne s'écrivent pas une fois pour toutes. Ils reflètent la valeur en vigueur, si bien que le cycle de vie doit les garder synchronisés — chaque opération qui touche un attribut protégé est aussi une transaction.
- Changer d'e-mail. 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 un compte. Supprime l'item de compte et les deux items de verrou dans une seule transaction, sinon tu abandonneras un verrou qui bloque la valeur pour toujours.
- Réessayer sans risque. Passe un
ClientRequestTokenpour qu'une transaction renvoyée (après une coupure réseau) soit idempotente plutôt qu'une double écriture.
Le piège est de traiter le verrou comme du poser-et-oublier. Un verrou créé à l'inscription mais jamais supprimé à la suppression du compte est une valeur que plus personne ne peut réutiliser — et cela ne se verra pas jusqu'à ce qu'un vrai utilisateur ne puisse pas récupérer son propre ancien nom d'utilisateur.
Étapes suivantes
Les marqueurs d'unicité sont un pattern de table unique, ils s'installent donc naturellement à
côté de tes autres items — lis conception à table unique pour la
disposition des clés, et Query vs Scan pour ne jamais te tourner vers un
Scan afin de vérifier un verrou. Le pattern a d'abord été détaillé lors de la session AWS
re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions.
Rédige les puts protégés par condition avec le Générateur d'expressions DynamoDB, puis essaie DynoTable pour inspecter les items de verrou sur ta propre table.


