Imporre l'unicità su più attributi in DynamoDB
DynamoDB garantisce l'unicità per esattamente una cosa: la primary key. Non esiste
vincolo UNIQUE (email), nessun UNIQUE (username), e nulla che copra
due attributi. Venendo da SQL, quell'assenza è la prima sorpresa — e il
primo punto dove la gente rilascia silenziosamente una race condition.
Come imporre un vincolo di unicità su più attributi in DynamoDB?
DynamoDB non ha alcun vincolo UNIQUE oltre alla , quindi imponi l'unicità tu stesso: modella ogni valore protetto come il proprio Item marker la cui chiave è quel valore, poi scrivi il record e ogni marker insieme in una sola TransactWriteItems, ogni put protetta da attribute_not_exists. La collisione che il motore già impone diventa il tuo vincolo.
- Non esiste un vincolo di unicità — solo la primary key è imposta come univoca dal motore. Ogni altro attributo "deve essere univoco" è compito tuo.
- Modella ogni regola di unicità come il proprio Item. Un Item marker dedicato la cui chiave è il valore che stai proteggendo trasforma "questa email è presa?" in una collisione di chiave che il motore già impone.
- Scrivili atomicamente con
TransactWriteItems. Una transazione, ogni put protetta daattribute_not_exists, così tutti i marker e il record reale fanno il commit insieme o nessuno lo fa. - Non check-then-write. Una lettura prima dell'insert è una race da manuale; due signup concorrenti leggono entrambi "libero" ed entrambi scrivono.
Perché l'approccio ovvio è sbagliato
L'istinto è fare una Query (o peggio, uno Scan) per l'email, non vedere nulla, poi
fare il PutItem del nuovo account. È una race check-then-act.
Due persone registrano ada@lovelace.io nello stesso millisecondo. Entrambe le letture tornano
vuote. Entrambe le scritture riescono. Hai ora due account su una email — e nulla
nella tabella lo segnala.
Un GSI su email non ti salva nemmeno. I GSI sono
eventualmente coerenti, quindi la lettura che fa da gate alla tua scrittura
può essere obsoleta per design. La soluzione non è un check più veloce; è far sì che la scrittura
stessa rifiuti di atterrare su un valore preso.
Modella ogni vincolo come Item marker
Il motore impone già una regola di unicità gratis: non puoi scrivere due Item con la stessa chiave. Quindi codifica ogni regola di unicità come una chiave.
Accanto all'Item account reale, scrivi un Item marker per ogni attributo protetto. La chiave di partizione del marker è il valore namespaced. Se il valore è preso, la chiave esiste, e una put protetta non può sovrascriverla.
Per un signup che deve mantenere univoci sia email che username, tre Item si
muovono insieme — con chiavi in un layout single-table (vedi
single-table design):
| Item | PK | SK | Scopo |
|---|---|---|---|
| Record account | ACCT#a1f9c3 | PROFILE | L'account reale |
| Lock email | UNIQ#EMAIL#ada@lovelace.io | LOCK | Riserva l'email |
| Lock username | UNIQ#HANDLE#ada | LOCK | Riserva lo username |
La PK dell'account è un id generato (ACCT#a1f9c3) — mai l'email — così
l'utente può cambiare la propria email più tardi senza riscrivere la primary key. Gli Item
lock non portano dati di profilo; esistono solo perché la loro chiave sia occupata.
Scrivi tutti e tre atomicamente
TransactWriteItems
applica fino a 100 scritture come un'unica unità tutto-o-niente. Proteggi ogni put con
attribute_not_exists(PK) così fallisce se quella chiave è già presente.
Se anche una sola condizione fallisce — il lock email, il lock handle o l'account
stesso — DynamoDB fa il rollback dell'intera transazione e lancia
TransactionCanceledException. Nessun signup parziale, nessun lock orfano.
{
"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 condizione è l'intero meccanismo. Senza attribute_not_exists, un secondo
signup con la stessa email sovrascrive silenziosamente il primo lock. Con essa, la put
rifiuta, la transazione si annulla, e la tua app fa emergere "email già in uso".
Costruire il ConditionExpression e la value map a mano è dove si insinuano i refusi.
Il DynamoDB Expression Builder emette la
condizione e l'Item tipizzato per ogni put così puoi incollare una transazione corretta
direttamente nella tua chiamata SDK.
Leggi il fallimento, non tirare a indovinare
Quando la transazione è annullata, DynamoDB restituisce un array CancellationReasons
posizionalmente — una voce per Item, in ordine di richiesta. Un ConditionalCheckFailed
nello slot 1 significa che l'email è presa; lo slot 2 significa che lo è lo username. Mappa lo slot
indietro a un errore preciso a livello di campo invece di un generico "signup fallito".
Ispeziona i lock in DynoTable
Gli Item marker sono invisibili nell'UI della tua app — sono plumbing. Quando un signup fallisce misteriosamente, devi vedere se il lock esiste davvero.
Apri la tabella in DynoTable e fai una Query sul prefisso UNIQ#. L'account e i suoi
due Item lock stanno insieme, così un signup bloccato (un lock lasciato indietro da una
delete fallita) è ovvio a colpo d'occhio.

Mantieni i lock onesti su modifica ed eliminazione
I lock non sono write-once. Rispecchiano il valore live, quindi il ciclo di vita deve mantenerli in sync — ogni operazione che tocca un attributo protetto è una transazione anch'essa.
- Cambio email. Una transazione: fai la put del nuovo lock
UNIQ#EMAIL#…conattribute_not_exists, elimina il vecchio lock, aggiorna l'account. Stessa garanzia tutto-o-niente. - Elimina account. Elimina l'Item account e entrambi gli Item lock in una sola transazione, altrimenti lascerai bloccato un lock che blocca il valore per sempre.
- Ritenta in sicurezza. Passa un
ClientRequestTokencosì una transazione reinviata (dopo un blip di rete) è idempotente invece che una doppia scrittura.
La trappola è trattare il lock come fire-and-forget. Un lock creato al signup ma mai eliminato alla rimozione dell'account è un valore che nessuno potrà mai riutilizzare — e non si manifesterà finché un utente reale non potrà rivendicare il proprio vecchio handle.
Prossimi passi
I marker di unicità sono un pattern single-table, quindi stanno naturalmente accanto ai tuoi
altri Item — leggi single-table design per il layout
delle chiavi, e Query vs Scan così da non reggiungere mai a uno
Scan per controllare un lock. Il pattern è stato illustrato per la prima volta nella sessione AWS
re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions.
Abbozza le put protette da condizione con il DynamoDB Expression Builder, poi prova DynoTable per ispezionare gli Item lock sulla tua tabella.


