Imporre l'unicità su più attributi in DynamoDB
DynamoDB garantisce l'unicità per una sola cosa: la . Non c'è
alcun vincolo UNIQUE (email), nessun UNIQUE (username) e niente che copra
due attributi. Provenendo da SQL, quell'assenza è la prima sorpresa — e il
primo punto in cui le persone mettono in produzione, in silenzio, una race condition.
Come si impone un vincolo di unicità su più attributi in DynamoDB?
DynamoDB non ha alcun vincolo UNIQUE oltre alla , quindi imponi l'unicità da solo: modella ogni valore protetto come un proprio item marcatore la cui chiave è quel valore, poi scrivi il record e ogni marcatore insieme in un unico TransactWriteItems, ciascun put protetto da attribute_not_exists. La collisione che il motore già impone diventa il tuo vincolo.
- Non esiste un vincolo di unicità — solo la chiave primaria è imposta come unica dal motore. Ogni altro attributo che "deve essere unico" è compito tuo.
- Modella ogni regola di unicità come un item a sé. Un item marcatore dedicato la cui chiave è il valore che stai proteggendo trasforma "questa email è già presa?" in una collisione di chiave che il motore già impone.
- Scrivili atomicamente con
TransactWriteItems. Una sola , ciascun put protetto daattribute_not_exists, così tutti i marcatori e il record reale vengono committati insieme o nessuno. - Non fare check-then-write. Una lettura prima dell'inserimento è una race da manuale; due iscrizioni concorrenti leggono entrambe "libero" e scrivono entrambe.
Perché l'approccio ovvio è sbagliato
L'istinto è fare Query (o peggio, Scan) per l'email, non vedere nulla, poi
fare PutItem del nuovo account. È una race di tipo check-then-act.
Due persone registrano ada@lovelace.io nello stesso millisecondo. Entrambe le letture
restituiscono vuoto. Entrambe le scritture riescono. Ora hai due account su un'unica
email — e niente nella tabella lo segnala.
Un su email non ti salva nemmeno. I GSI sono
a coerenza finale, quindi la lettura che fa da guardia alla tua
scrittura può essere stale per progetto. La soluzione non è un controllo più veloce; è
fare in modo che sia la scrittura stessa a rifiutarsi di finire su un valore già preso.
Modella ogni vincolo come un item marcatore
Il motore già impone gratis una regola di unicità: 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 marcatore per ogni attributo protetto. La chiave di partizione del marcatore è il valore con namespace. Se il valore è preso, la chiave esiste, e un put protetto non può sovrascriverlo.
Per un'iscrizione che deve mantenere unici sia email sia username, tre item si
muovono insieme — con le 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 l'username |
La PK dell'account è un id generato (ACCT#a1f9c3) — mai l'email — così
l'utente può cambiare l'email in seguito senza riscrivere la chiave primaria. 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 dell'email, il lock dell'handle o
l'account stesso — DynamoDB fa il rollback dell'intera transazione e lancia
TransactionCanceledException. Nessuna iscrizione 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, una seconda
iscrizione con la stessa email sovrascrive silenziosamente il primo lock. Con essa, il put
si rifiuta, la transazione si annulla e la tua app mostra "email già in uso".
Costruire a mano la ConditionExpression e la mappa dei valori è 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 viene annullata, DynamoDB restituisce un array CancellationReasons
in modo posizionale — una voce per item, nell'ordine della richiesta. Un ConditionalCheckFailed
nello slot 1 significa che l'email è presa; lo slot 2 significa che è l'username. Mappa lo slot
di nuovo a un errore preciso, a livello di campo, invece di un generico "iscrizione fallita".
Ispeziona i lock in DynoTable
Gli item marcatore sono invisibili nell'interfaccia della tua app — sono impianto tecnico. Quando un'iscrizione fallisce misteriosamente, hai bisogno di vedere se il lock esiste davvero.
Apri la tabella in DynoTable e fai Query sul prefisso UNIQ#. L'account e i suoi
due item lock stanno insieme, così un'iscrizione bloccata (un lock lasciato indietro da
una delete andata storta) è ovvia a colpo d'occhio.

Mantieni i lock onesti su modifica ed eliminazione
I lock non sono write-once. Rispecchiano il valore attivo, quindi il ciclo di vita deve mantenerli sincronizzati — ogni operazione che tocca un attributo protetto è anche una transazione.
- Cambio email. Una sola transazione: metti il 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, o abbandonerai un lock che blocca il valore per sempre.
- Riprova in sicurezza. Passa un
ClientRequestTokencosì una transazione reinviata (dopo un intoppo di rete) è idempotente invece di essere una doppia scrittura.
La trappola è trattare il lock come fire-and-forget. Un lock creato all'iscrizione ma mai eliminato alla rimozione dell'account è un valore che nessuno potrà mai riutilizzare — e non salterà fuori finché un utente reale non riuscirà a reclamare il suo vecchio handle.
Passaggi successivi
I marcatori 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ì non ricorri mai a uno
Scan per controllare un lock. Il pattern è stato illustrato per la prima volta nella
sessione di AWS re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions.
Prepara i put protetti da condizioni con il DynamoDB Expression Builder, poi prova DynoTable per ispezionare gli item lock contro la tua tabella.


