Impondo unicidade em múltiplos atributos no DynamoDB
O DynamoDB garante unicidade para exatamente uma coisa: a chave primária. Não há
restrição UNIQUE (email), nem UNIQUE (username), e nada que abranja dois atributos.
Vindo do SQL, essa ausência é a primeira surpresa — e o primeiro lugar onde as pessoas
silenciosamente lançam uma condição de corrida.
Como impor uma restrição de unicidade em múltiplos atributos no DynamoDB?
O DynamoDB não tem restrição UNIQUE além da chave primária, então você impõe a unicidade por conta própria: modele cada valor protegido como seu próprio item marcador cuja chave é esse valor, depois escreva o registro e todos os marcadores juntos em um único TransactWriteItems, cada put protegido por attribute_not_exists. A colisão que o engine já impõe torna-se a sua restrição.
- Não existe restrição de unicidade — só a chave primária é imposta como única pelo engine. Todo outro atributo "precisa ser único" é trabalho seu.
- Modele cada regra de unicidade como seu próprio item. Um item marcador dedicado cuja chave é o valor que você está protegendo transforma "esse email está em uso?" em uma colisão de chave que o engine já impõe.
- Escreva-os atomicamente com
TransactWriteItems. Uma transação, cada put protegido porattribute_not_exists, para que todos os marcadores e o registro real sejam commitados juntos ou nenhum seja. - Não verifique-depois-escreva. Uma leitura-antes-de-inserir é uma corrida de manual; dois cadastros concorrentes ambos leem "livre" e ambos escrevem.
Por que a abordagem óbvia está errada
O instinto é fazer Query (ou pior, Scan) pelo email, não ver nada, depois PutItem
da nova conta. Isso é uma corrida verificar-depois-agir.
Duas pessoas registram ada@lovelace.io no mesmo milissegundo. Ambas as leituras
retornam vazio. Ambas as escritas têm sucesso. Você agora tem duas contas em um email —
e nada na tabela sinaliza isso.
Um GSI em email também não te salva. GSIs são
eventualmente consistentes, então a leitura que controla sua
escrita pode estar desatualizada por design. A correção não é uma verificação mais
rápida; é fazer a própria escrita se recusar a cair em um valor já tomado.
Modele cada restrição como um item marcador
O engine já impõe uma regra de unicidade de graça: você não pode escrever dois itens com a mesma chave. Então codifique toda regra de unicidade como uma chave.
Ao lado do item de conta real, escreva um item marcador por atributo protegido. A chave de partição do marcador é o valor com namespace. Se o valor está tomado, a chave existe, e um put protegido não pode sobrescrevê-la.
Para um cadastro que precisa manter tanto email quanto username únicos, três itens
se movem juntos — chaveados em um layout single-table (veja
single-table design):
| Item | PK | SK | Propósito |
|---|---|---|---|
| Registro da conta | ACCT#a1f9c3 | PROFILE | A conta real |
| Trava de email | UNIQ#EMAIL#ada@lovelace.io | LOCK | Reserva o email |
| Trava de username | UNIQ#HANDLE#ada | LOCK | Reserva o username |
O próprio PK da conta é um id gerado (ACCT#a1f9c3) — nunca o email — para que o
usuário possa mudar seu email depois sem reescrever a chave primária. Os itens de trava
não carregam dados de perfil; eles existem só para que sua chave esteja ocupada.
Escreva os três atomicamente
TransactWriteItems
aplica até 100 escritas como uma unidade tudo-ou-nada. Proteja cada put com
attribute_not_exists(PK) para que ele falhe se aquela chave já estiver presente.
Se qualquer condição falhar — a trava de email, a trava de handle ou a própria conta —
o DynamoDB reverte a transação inteira e lança TransactionCanceledException. Sem
cadastro parcial, sem trava órfã.
{
"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)"
}
}
]
}A condição é o mecanismo inteiro. Sem attribute_not_exists, um segundo cadastro com o
mesmo email sobrescreve silenciosamente a primeira trava. Com ela, o put se recusa, a
transação cancela, e seu app exibe "email já em uso".
Construir a ConditionExpression e o mapa de valores à mão é onde os erros de digitação
se infiltram. O DynamoDB Expression Builder emite
a condição e o Item tipado para cada put, para que você cole uma transação correta
direto na sua chamada de SDK.
Leia a falha, não chute sobre ela
Quando a transação é cancelada, o DynamoDB retorna um array CancellationReasons
posicionalmente — uma entrada por item, na ordem da requisição. Um
ConditionalCheckFailed no slot 1 significa que o email está tomado; o slot 2 significa
que o username está. Mapeie o slot de volta para um erro preciso, em nível de campo, em
vez de um genérico "cadastro falhou".
Inspecione as travas no DynoTable
Os itens marcadores são invisíveis na UI do seu app — são encanamento. Quando um cadastro falha misteriosamente, você precisa ver se a trava de fato existe.
Abra a tabela no DynoTable e dê Query no prefixo UNIQ#. A conta e seus dois itens de
trava ficam juntos, então um cadastro travado (uma trava deixada para trás por uma
exclusão malfeita) fica óbvio à primeira vista.

Mantenha as travas honestas na mudança e exclusão
Travas não são escreva-uma-vez. Elas espelham o valor vivo, então o ciclo de vida tem que mantê-las em sincronia — toda operação que toca um atributo protegido também é uma transação.
- Mudar email. Uma transação: put da nova trava
UNIQ#EMAIL#…comattribute_not_exists, exclua a trava antiga, atualize a conta. Mesma garantia tudo-ou-nada. - Excluir conta. Exclua o item de conta e ambos os itens de trava em uma transação, ou você vai deixar encalhada uma trava que bloqueia o valor para sempre.
- Repita com segurança. Passe um
ClientRequestTokenpara que uma transação reenviada (após uma falha de rede) seja idempotente em vez de uma escrita dupla.
A cilada é tratar a trava como dispare-e-esqueça. Uma trava criada no cadastro mas nunca excluída na remoção da conta é um valor que ninguém jamais pode reutilizar — e isso não vai aparecer até um usuário real não conseguir reivindicar seu próprio handle antigo.
Próximos passos
Marcadores de unicidade são um padrão single-table, então ficam naturalmente ao lado
dos seus outros itens — leia single-table design para o
layout das chaves, e Query vs Scan para que você nunca recorra
a um Scan para verificar uma trava. O padrão foi percorrido pela primeira vez na
sessão re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions da AWS.
Rascunhe os puts protegidos por condição com o DynamoDB Expression Builder, depois experimente o DynoTable para inspecionar os itens de trava contra sua própria tabela.


