Unicidad en varios atributos de DynamoDB
DynamoDB garantiza la unicidad de exactamente una cosa: la . No hay
restricción UNIQUE (email), ni UNIQUE (username), ni nada que abarque dos atributos.
Viniendo de SQL, esa ausencia es la primera sorpresa — y el primer sitio donde la gente
se lleva calladamente una condición de carrera a producción.
¿Cómo se impone una restricción de unicidad sobre varios atributos en DynamoDB?
DynamoDB no tiene ninguna restricción UNIQUE más allá de la , así que impones la unicidad tú mismo: modela cada valor protegido como su propio elemento marcador cuya clave es ese valor, luego escribe el registro y cada marcador juntos en un único TransactWriteItems, cada put protegido por attribute_not_exists. La colisión que el motor ya impone se convierte en tu restricción.
- No hay restricción de unicidad — solo la clave primaria la impone el motor como única. Cualquier otro atributo que "debe ser único" es cosa tuya.
- Modela cada regla de unicidad como su propio elemento. Un elemento marcador dedicado cuya clave es el valor que proteges convierte "¿está cogido este email?" en una colisión de clave que el motor ya impone.
- Escríbelos atómicamente con
TransactWriteItems. Una , cada put protegido porattribute_not_exists, de modo que todos los marcadores y el registro real se confirman juntos o ninguno lo hace. - No compruebes-y-luego-escribas. Una lectura antes de insertar es una condición de carrera de manual; dos registros concurrentes leen ambos "libre" y ambos escriben.
Por qué el enfoque obvio es incorrecto
El instinto es hacer Query (o peor, Scan) del email, no ver nada y luego PutItem
de la nueva cuenta. Eso es una carrera comprobar-y-actuar.
Dos personas registran ada@lovelace.io en el mismo milisegundo. Ambas lecturas
devuelven vacío. Ambas escrituras tienen éxito. Ahora tienes dos cuentas con un mismo
email — y nada en la tabla lo señala.
Un sobre email tampoco te salva. Los GSI son
eventualmente consistentes, así que la lectura que controla tu
escritura puede estar obsoleta por diseño. El arreglo no es una comprobación más rápida;
es hacer que la propia escritura se niegue a aterrizar sobre un valor cogido.
Modela cada restricción como un elemento marcador
El motor ya impone una regla de unicidad gratis: no puedes escribir dos elementos con la misma clave. Así que codifica cada regla de unicidad como una clave.
Junto al elemento de cuenta real, escribe un elemento marcador por cada atributo protegido. La clave de partición del marcador es el valor con espacio de nombres. Si el valor está cogido, la clave existe, y un put protegido no puede sobrescribirla.
Para un registro que debe mantener únicos tanto email como username, tres elementos
se mueven juntos — con clave en un diseño de tabla única (ver
diseño de tabla única):
| Elemento | PK | SK | Propósito |
|---|---|---|---|
| Registro de cuenta | ACCT#a1f9c3 | PROFILE | La cuenta real |
| Bloqueo de email | UNIQ#EMAIL#ada@lovelace.io | LOCK | Reserva el email |
| Bloqueo de usuario | UNIQ#HANDLE#ada | LOCK | Reserva el nombre de usuario |
El propio PK de la cuenta es un id generado (ACCT#a1f9c3) — nunca el email — para que
el usuario pueda cambiar su email más tarde sin reescribir la clave primaria. Los
elementos de bloqueo no llevan datos de perfil; existen solo para que su clave esté
ocupada.
Escribe los tres atómicamente
TransactWriteItems
aplica hasta 100 escrituras como una unidad todo-o-nada. Protege cada put con
attribute_not_exists(PK) para que falle si esa clave ya está presente.
Si falla cualquiera de las condiciones — el bloqueo de email, el bloqueo de nombre o la
propia cuenta — DynamoDB revierte toda la transacción y lanza
TransactionCanceledException. Ni registro parcial, ni bloqueo huérfano.
{
"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 condición es todo el mecanismo. Sin attribute_not_exists, un segundo registro con el
mismo email sobrescribe en silencio el primer bloqueo. Con ella, el put se niega, la
transacción se cancela y tu app muestra "el email ya está en uso".
Construir la ConditionExpression y el mapa de valores a mano es donde se cuelan los
errores de tecleo. El constructor de expresiones de DynamoDB emite la
condición y el Item tipado de cada put para que pegues una transacción correcta
directamente en tu llamada al SDK.
Lee el fallo, no lo adivines
Cuando la transacción se cancela, DynamoDB devuelve un array CancellationReasons
posicionalmente — una entrada por elemento, en el orden de la petición. Un
ConditionalCheckFailed en la posición 1 significa que el email está cogido; la posición
2 significa que lo está el nombre de usuario. Mapea la posición de vuelta a un error
preciso, a nivel de campo, en lugar de un genérico "el registro falló".
Inspecciona los bloqueos en DynoTable
Los elementos marcador son invisibles en la UI de tu app — son fontanería. Cuando un registro falla misteriosamente, necesitas ver si el bloqueo existe de verdad.
Abre la tabla en DynoTable y haz Query del prefijo UNIQ#. La cuenta y sus dos
elementos de bloqueo están juntos, así que un registro atascado (un bloqueo dejado atrás
por un borrado fallido) es obvio de un vistazo.

Mantén los bloqueos honestos al cambiar y borrar
Los bloqueos no son de una sola escritura. Reflejan el valor vivo, así que el ciclo de vida tiene que mantenerlos sincronizados — cada operación que toca un atributo protegido es también una transacción.
- Cambiar email. Una transacción: pon el nuevo bloqueo
UNIQ#EMAIL#…conattribute_not_exists, borra el bloqueo antiguo, actualiza la cuenta. La misma garantía todo-o-nada. - Borrar cuenta. Borra el elemento de cuenta y ambos elementos de bloqueo en una transacción, o dejarás un bloqueo varado que bloquea el valor para siempre.
- Reintenta con seguridad. Pasa un
ClientRequestTokenpara que una transacción reenviada (tras un corte de red) sea idempotente en lugar de una doble escritura.
La trampa es tratar el bloqueo como algo de "poner y olvidar". Un bloqueo creado en el registro pero nunca borrado al eliminar la cuenta es un valor que nadie podrá reutilizar jamás — y no saldrá a la luz hasta que un usuario real no pueda reclamar su propio nombre de usuario antiguo.
Próximos pasos
Los marcadores de unicidad son un patrón de tabla única, así que conviven de forma
natural junto a tus otros elementos — lee diseño de tabla única
para el diseño de claves, y Query vs Scan para que nunca recurras
a un Scan para comprobar un bloqueo. El patrón se recorrió por primera vez en la sesión
de AWS re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions.
Redacta los puts protegidos por condición con el constructor de expresiones de DynamoDB, luego prueba DynoTable para inspeccionar los elementos de bloqueo contra tu propia tabla.


