Intermedio6 min de lectura

Condition expressions de DynamoDB

Una condition expression es un predicado que DynamoDB evalúa sobre el elemento existente antes de confirmar tu escritura. Si el predicado es falso, la escritura se rechaza y nada cambia. Es lo más parecido que DynamoDB tiene a una cláusula WHERE sobre una escritura — y la única forma segura de imponer un invariante.

¿Cómo funcionan las condition expressions de DynamoDB?

Una condition expression es un predicado que DynamoDB evalúa del lado del servidor sobre el elemento actual antes de confirmar una escritura. Si es verdadero, la escritura continúa; si es falso, la escritura se rechaza con ConditionalCheckFailedException y nada cambia. Pliega la comprobación y la mutación en una sola operación atómica, de modo que los llamantes concurrentes no pueden competir contra una lectura obsoleta.

  • Es una guarda, no un filtro. La ConditionExpression se ejecuta del lado del servidor sobre el elemento actual; un resultado falso hace fallar la escritura con ConditionalCheckFailedException.
  • Reemplaza al leer-luego-escribir. Sin viaje de ida y vuelta SELECT y luego UPDATE — la comprobación y la mutación son una operación atómica, así que dos llamantes no pueden competir.
  • Rechazar es gratis, ejecutar no. Una escritura condicional fallida aún consume capacidad de escritura. La garantía cuesta lo mismo que la escritura que bloquea.

Viniendo de SQL, leerías la fila, la comprobarías en el código de la app, luego actualizarías. En DynamoDB esa brecha entre lectura y escritura es un bug de corrupción de datos esperando a un llamante concurrente. La condition expression cierra la brecha.

Dónde se aplican

Adjuntas una ConditionExpression a PutItem, UpdateItem, DeleteItem y a cada acción dentro de TransactWriteItems. No forma parte de Query ni de Scan — esos usan FilterExpression, que es algo distinto en la ruta de lectura.

Esa distinción confunde a la gente, así que sé preciso:

ConditionExpressionFilterExpression
RutaEscrituras (Put/Update/Delete)Lecturas (Query/Scan)
Efecto al fallarRechaza toda la escrituraDescarta el elemento de los resultados
VeEl elemento actual, pre-escrituraCada elemento candidato, post-lectura
CosteLa escritura fallida aún facturaLos elementos filtrados aún se facturan en la lectura

Ambos se ejecutan del lado del servidor. La diferencia es lo que hace "falso": una condición aborta una mutación; un filtro solo oculta una fila que ya pagaste por leer. (AWS: Condition Expressions)

Las funciones que realmente usarás

El lenguaje de condiciones es pequeño. Los caballos de batalla:

  • attribute_exists(path) / attribute_not_exists(path) — ¿existe este atributo en el elemento? El idioma clásico para "crear solo si está ausente" / "actualizar solo si está presente".
  • Comparadores — =, <>, <, <=, >, >= — contra un valor u otro atributo.
  • attribute_type, begins_with, contains, size — comprobaciones de tipo y de cadena/conjunto.
  • BETWEEN … AND …, IN (…) — rango y pertenencia.
  • AND, OR, NOT, paréntesis — para combinar lo anterior.

attribute_not_exists sobre la partition key es la forma canónica de hacer que PutItem se comporte como una inserción que no pisará un elemento existente — DynamoDB no tiene una operación "insert" separada, así que la condición es la semántica de inserción. (AWS: Comparison Operator and Function Reference)

Un ejemplo desarrollado: proteger un libro mayor contra el descubierto

Toma un libro mayor bancario. Cada cuenta es un elemento:

PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents    = 0

El invariante: un cargo nunca debe empujar el saldo disponible por debajo de cero, y nunca debes cargar a una cuenta que no existe. Dos reglas, ambas imponibles en la propia escritura.

La forma equivocada (el tiro al pie)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← app-side check
UpdateItem  SET clearedCents = 20000

Entre el GetItem y el UpdateItem, un segundo cargo puede leer el mismo 50000, pasar su propia comprobación y escribir también. Ambos tienen éxito; la cuenta se vuelve negativa. Esta es una carrera de lectura-modificación-escritura, y ninguna cantidad de validación del lado de la app la arregla — la comprobación y la escritura son operaciones separadas.

La forma correcta

Pliega la comprobación dentro de la escritura. Carga 30000 céntimos, condicionado a que la cuenta exista y tenga suficiente:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = clearedCents - :amt
  ConditionExpression:
    attribute_exists(PK) AND clearedCents >= :amt

con :amt = 30000. Si el saldo es demasiado bajo, o el elemento nunca se creó, DynamoDB rechaza la escritura con ConditionalCheckFailedException y el saldo queda intacto. El cargo concurrente o bien ve el saldo original y se comprueba contra él, o bien ve el actualizado — nunca una lectura obsoleta sobre la que actuó.

Puedes construir y copiar la expresión exacta — nombres, valores y todo — con el constructor de expresiones de DynamoDB en lugar de ensamblar a mano el mapa ExpressionAttributeValues.

Inspeccionar la guarda en DynoTable

Cuando una escritura condicional falla, quieres ver el estado real del elemento, no adivinarlo. Saca a la vista el elemento de la cuenta y lee clearedCents directamente.

La colección del libro mayor en DynoTable — el item BALANCE muestra clearedCents por encima de los items de transacción de la cuenta.
La colección del libro mayor en DynoTable — el item BALANCE muestra clearedCents por encima de los items de transacción de la cuenta.

Lee el rechazo, no reintentes a ciegas

ConditionalCheckFailedException no es un error transitorio — reintentar la misma escritura no cambia nada. Significa que se disparó una regla de negocio: fondos insuficientes, creación duplicada, versión obsoleta. Exponlo como un resultado de dominio, no como un hipo de infraestructura.

Dos cosas hacen depurables los fallos:

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB devuelve el elemento actual junto con el fallo, para que puedas mostrar "el saldo era 20000, pediste 30000" sin una segunda lectura. (AWS: Working with Items)
  • Distinguir las dos razones del fallo. attribute_exists(PK) AND clearedCents >= :amt colapsa "sin cuenta" y "sin fondos" en una sola excepción. Si los llamantes necesitan diferenciarlas, divide en dos escrituras o inspecciona el elemento devuelto.

El bloqueo optimista es el mismo truco

El patrón del número de versión es solo una condition expression con otro sombrero. Almacena un atributo version; cada escritura afirma la versión que leíste y la incrementa:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = :new, version = :next
  ConditionExpression: version = :seen

Si otro escritor se movió primero, version = :seen es falso, la escritura se rechaza, y relees y reintentas. Así es como DynamoDB hace el control de concurrencia sin bloqueos — afirma lo que viste, falla si se movió. (AWS: Optimistic Locking with Version Number)

Trampas y próximos pasos

  • Nombres que chocan con palabras reservadas. status, size, name y ~570 más están reservados. Ponles un alias con ExpressionAttributeNames (#s = status) o tu expresión fallará silenciosamente al analizarse.
  • Una condición no puede referenciar otro elemento. Solo ve el elemento que se está escribiendo. Los invariantes entre elementos necesitan TransactWriteItems con una ConditionExpression por acción, o un ConditionCheck contra un elemento centinela.
  • Las escrituras fallidas aún cuestan WCU. Una guarda que rechaza el 90% de las veces aún factura por esos rechazos. Seguro barato, pero no gratis.

Para modelar las claves contra las que se ejecutan estas guardas, consulta diseño de tabla única y Query vs Scan. Cuando estés listo para emitir escrituras condicionales contra datos reales, descarga DynoTable y ejecútalas contra tus propias tablas.

Actualizado