Unicidade em múltiplos atributos no DynamoDB
O DynamoDB garante unicidade para exatamente uma coisa: a . Não há
restrição UNIQUE (email), nenhuma 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 colocam uma condição de corrida em produção.
Como você impõe uma restrição de unicidade sobre múltiplos atributos no DynamoDB?
O DynamoDB não tem restrição UNIQUE além da , então você impõe a unicidade por conta própria: modele cada valor protegido como seu próprio item marcador cuja chave é aquele valor, depois escreva o registro e cada marcador juntos em um único TransactWriteItems, cada put protegido por attribute_not_exists. A colisão que o mecanismo já impõe se torna a sua restrição.
- Não existe restrição de unicidade — apenas a chave primária é imposta como única pelo mecanismo. Todo outro atributo que "precisa ser único" é tarefa sua.
- Modele cada regra de unicidade como seu próprio item. Um item marcador dedicado cuja chave é o valor que você está protegendo transforma "este e-mail está em uso?" em uma colisão de chave que o mecanismo já impõe.
- Escreva-os atomicamente com
TransactWriteItems. Uma , cada put protegido porattribute_not_exists, para que todos os marcadores e o registro real sejam confirmados juntos ou nenhum seja. - Não verifique-depois-escreva. Um read-before-insert é uma condição de corrida clássica; dois cadastros concorrentes ambos leem "livre" e ambos escrevem.
Por que a abordagem óbvia está errada
O instinto é fazer Query (ou pior, Scan) pelo e-mail, não ver nada e então
PutItem da nova conta. Isso é uma corrida check-then-act.
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 e-mail — e nada
na tabela sinaliza isso.
Uma 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 com que a própria escrita
se recuse a pousar sobre um valor já usado.
Modele cada restrição como um item marcador
O mecanismo já impõe uma regra de unicidade de graça: você não pode escrever dois itens com a mesma chave. Então codifique cada 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 estiver em uso, a chave existe, e um put protegido não consegue sobrescrevê-la.
Para um cadastro que precisa manter tanto email quanto username únicos, três itens se movem
juntos — com chaves em um layout de tabela única (veja
design de tabela única):
| Item | PK | SK | Propósito |
|---|---|---|---|
| Registro da conta | ACCT#a1f9c3 | PROFILE | A conta real |
| Bloqueio de e-mail | UNIQ#EMAIL#ada@lovelace.io | LOCK | Reserva o e-mail |
| Bloqueio de usuário | UNIQ#HANDLE#ada | LOCK | Reserva o nome de usuário |
O PK da própria conta é um id gerado (ACCT#a1f9c3) — nunca o e-mail — para que
o usuário possa mudar seu e-mail depois sem reescrever a chave primária. Os itens de bloqueio
não carregam dados de perfil; existem apenas para que sua chave esteja ocupada.
Escreva os três atomicamente
O TransactWriteItems
aplica até 100 escritas como uma única unidade tudo-ou-nada. Proteja cada put com
attribute_not_exists(PK) para que ele falhe se aquela chave já estiver presente.
Se qualquer uma das condições falhar — o bloqueio de e-mail, o bloqueio de usuário ou a própria
conta — o DynamoDB reverte toda a transação e lança
TransactionCanceledException. Sem cadastro parcial, sem bloqueio órfão.
{
"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 e-mail sobrescreve silenciosamente o primeiro bloqueio. Com ela, o put
se recusa, a transação é cancelada, e seu app expõe "e-mail já em uso".
Construir a ConditionExpression e o mapa de valores à mão é onde os erros de digitação surgem.
O construtor de expressões do DynamoDB 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 adivinhe
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 e-mail está em uso; o slot 2 significa que o nome de usuário está. Mapeie o slot
de volta para um erro preciso, em nível de campo, em vez de um genérico "cadastro falhou".
Inspecione os bloqueios 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 o bloqueio realmente existe.
Abra a tabela no DynoTable e faça Query do prefixo UNIQ#. A conta e seus
dois itens de bloqueio ficam juntos, então um cadastro travado (um bloqueio deixado para trás por um
delete malfeito) fica óbvio de imediato.

Mantenha os bloqueios honestos na alteração e na exclusão
Bloqueios não são write-once. Eles espelham o valor ativo, então o ciclo de vida precisa mantê-los sincronizados — toda operação que toca um atributo protegido também é uma transação.
- Trocar o e-mail. Uma transação: escreva o novo bloqueio
UNIQ#EMAIL#…comattribute_not_exists, delete o bloqueio antigo, atualize a conta. Mesma garantia tudo-ou-nada. - Deletar a conta. Delete o item de conta e ambos os itens de bloqueio em uma transação, ou você deixará órfão um bloqueio que trava o valor para sempre.
- Faça retry com segurança. Passe um
ClientRequestTokenpara que uma transação reenviada (após um soluço de rede) seja idempotente em vez de uma escrita dupla.
A armadilha é tratar o bloqueio como fire-and-forget. Um bloqueio criado no cadastro mas nunca deletado na remoção da conta é um valor que ninguém consegue reutilizar — e não vai aparecer até que um usuário real não consiga reivindicar seu próprio nome de usuário antigo.
Próximos passos
Marcadores de unicidade são um padrão de tabela única, então ficam naturalmente ao lado dos seus
outros itens — leia design de tabela única para o layout
de chaves, e Query vs Scan para nunca recorrer a um
Scan para verificar um bloqueio. O padrão foi apresentado pela primeira vez na sessão
re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions da AWS.
Esboce os puts protegidos por condição com o construtor de expressões do DynamoDB, depois experimente o DynoTable para inspecionar os itens de bloqueio na sua própria tabela.


