Blog de Amazon Web Services (AWS)

Simulando restricciones únicas de Amazon DynamoDB mediante transacciones

Por Chad Tindel, Arquitecto de Soluciones Especialista en DynamoDB  en AWS y
Brett Hensley, Arquitecto de Soluciones en AWS

 

La mayoría de los sistemas de bases de datos relacionales, y algunos sistemas de bases de datos no relacionales, tienen una construcción conocida como clave única o restricción única. Esta característica garantiza que todos los valores de una columna o campo sean únicos en las filas.

Por ejemplo, si usted tiene una tabla User (de usuarios), tal vez tenga un UUID como clave primaria que identifica de forma única a cada usuario, pero también puede ser que tenga campos de nombre de usuario y correo electrónico («atributos» en la terminología de DynamoDB), que también deben ser únicos para ese usuario. Este caso de uso se mencionó en la sesión del AWS Summit 2018 DAT374 sobre transacciones de DynamoDB.

En Amazon DynamoDB, la clave primaria es la clave de partición (si no se elige ninguna clave de ordenamiento para la tabla) o la combinación de la clave de partición y ordenamiento. Se garantiza que las claves primarias son únicas dentro de una tabla. Sin embargo, DynamoDB no tiene un mecanismo integrado para garantizar la exclusividad de los atributos que no son la clave primaria.

Esta publicación describe un patrón que se utiliza para implementar este tipo de singularidad desde el lado de la aplicación. Le mostramos ejemplos de cómo crear, actualizar y eliminar elementos al usar este patrón en un diseño de esquema de una sola tabla.

Perspectiva de la solución

Utilizando el ejemplo anterior, imagine que tiene una tabla User y esa tabla tiene atributos como los siguientes:

  • pk (clave primaria almacenada como UUID)
  • userName (nombre de usuario)
  • email
  • fullName (nombre completo)
  • phoneNumber (número de teléfono)

Los atributos UUID, userName y email deben ser únicos, pero fullName y phoneNumber no deben serlo. Varias personas pueden compartir el mismo número de teléfono residencial. A continuación se muestran algunas filas de muestras.

pk userName email fullName phoneNumber
b201c1f2-238e-461f-88e6-0e606fbc3c51 btables bobby.tables@gmail.com Bobby Tables +1-202-555-0124
8ec436a8-97e6-4e72-aec2-b47668e96a94 jsmith johnsmith@yahoo.com John Smith +1-404-555-9325
eed78b78-29f9-4893-a432-4c4f50b0d1c4 phonork pphonork Peter Phonorkus +1-805-555-0820

Debido a que DynamoDB ya garantiza que el atributo pk sea único, usted necesita un mecanismo para asegurarse de que los atributos email y userName también sean únicos.

Para hacer esto, inserte elementos adicionales en la misma tabla, con el atributo pk establecido con el nombre del atributo y el valor del elemento, delimitado por un signo de numeral (#). La nueva tabla se parece al siguiente ejemplo:

pk userName email fullName phoneNumber
b201c1f2-238e-461f-88e6-0e606fbc3c51 btables bobby.tables@gmail.com Bobby Tables +1-202-555-0124
Eiifccredjgflljggdencnghinefikkhdfcdjidtnlfj
email#bobby.tables@gmail.com
8ec436a8-97e6-4e72-aec2-b47668e96a94 jsmith johnsmith@yahoo.com John Smith +1-404-555-9325
userName#jsmith
email#johnsmith@yahoo.com
eed78b78-29f9-4893-a432-4c4f50b0d1c4 phonork pphonork@calpoly.edu Peter Phonorkus +1-805-555-0820
userName#phonork
email#pphonork@calpoly.edu

Cada vez que usted inserte un nuevo elemento en la tabla, también deberá insertar los otros dos elementos. Esto garantiza la exclusividad de los atributos de email y userName. Del mismo modo, si usted elimina un elemento, debe eliminar los otros dos elementos correspondientes.

Finalmente, si usted modifica uno de los atributos únicos (como en el caso en el que el usuario desee cambiar su dirección de correo electrónico en su cuenta), usted también deberá actualizar los elementos únicos relacionados. Todas estas modificaciones deben estar unidas en una transacción de DynamoDB para que todas tengan éxito o fallen juntas.

Ejemplos de línea de comando

A continuación, usted examinará algunos ejemplos de línea de comando para implementar este diseño. Estos ejemplos de CLI se pueden migrar fácilmente a cualquiera de los SDK de DynamoDB de su lenguaje de programación preferido.

Imagine que tiene una tabla de usuarios y esta está vacía. Registre su primer usuario insertando sus tres filas como parte de una transacción y use un identificador de transacción para garantizar la idempotencia. Este identificador de transacción (el token de solicitud del cliente) le permite enviar una transacción idéntica más de una vez, tal vez en el caso de una aplicación reiniciada o reanudada, y aún así terminar con el mismo resultado.

aws dynamodb transact-write-items --client-request-token TRANSACTION1 --transact-items '[
{
    "Put": {
      "TableName" : "User", 
      "ConditionExpression": "attribute_not_exists(pk)",
        "Item" : {
          "pk":{"S":"b201c1f2-238e-461f-88e6-0e606fbc3c51"},
          "userName":{"S":"btables"},
          "email":{"S":"bobby.tables@gmail.com"},
          "fullName":{"S":"Bobby Tables"},
          "phoneNumber":{"S":"+1-202-555-0124"}
       }
    }
},
  {
    "Put": {
      "TableName" : "User", 
      "ConditionExpression": "attribute_not_exists(pk)",
      "Item" : {
        "pk":{"S":"userName#btables"}
       }
    }
},
  {
    "Put": {
      "TableName" : "User", 
      "ConditionExpression": "attribute_not_exists(pk)",
      "Item" : {
        "pk":{"S":"email#bobby.tables@gmail.com"}
       }
    }
}

Ahora, si enumera los elementos, puede ver que se han creado los tres elementos.

aws dynamodb scan --table-name User
{
    "Count": 3,
    "Items": [
        {
            "userName": {
                "S": "btables"
            },
            "pk": {
                "S": "b201c1f2-238e-461f-88e6-0e606fbc3c51"
            },
            "fullName": {
                "S": "Bobby Tables"
            },
            "phoneNumber": {
                "S": "+1-202-555-0124"
            },
            "email": {
                "S": "bobby.tables@gmail.com"
            }
        },
        {
            "pk": {
                "S": "email#bobby.tables@gmail.com"
            }
        },
        {
            "pk": {
                "S": "userName#btables"
            }
        }
    ],
    "ScannedCount": 3,
    "ConsumedCapacity": null
}


A continuación se muestra lo que sucede si un Bobby Tables falso intenta registrarse con la misma dirección de correo electrónico.

 

aws dynamodb transact-write-items --client-request-token TRANSACTION2 --transact-items '[
   {
     "Put": {
       "TableName" : "User",
       "ConditionExpression": "attribute_not_exists(pk)",
         "Item" : {
           "pk":{"S":"8ec436a8-97e6-4e72-aec2-b47668e96a94"},
           "userName":{"S":"caulfield"},
           "email":{"S":"bobby.tables@gmail.com"},
           "fullName":{"S":"Phony Bobby Tables"},
           "phoneNumber":{"S":"+1-202-555-0124"}
        }
     }
 },
   {
     "Put": {
       "TableName" : "User",
       "ConditionExpression": "attribute_not_exists(pk)",
       "Item" : {
         "pk":{"S":"userName#caulfield"}
        }
     }
 },
   {
     "Put": {
       "TableName" : "User",
       "ConditionExpression": "attribute_not_exists(pk)",
       "Item" : {
         "pk":{"S":"email#bobby.tables@gmail.com"}
        }
     }
  }
]'

An error occurred (TransactionCanceledException) when calling the TransactWriteItems operation: Transaction cancelled, please refer cancellation reasons for specific reasons [None, None, ConditionalCheckFailed]

Como podrá ver, esta transacción falla porque el tercer elemento de la transacción recibió un error de ConditionalCheckFailed porque ya existía un elemento con el valor pk de “email#bobby.tables@gmail.com”.

Si el pequeño Bobby Tables quiere registrar un dominio personalizado y cambiar su dirección de correo electrónico almacenada, usted solo debe modificar dos elementos. Pero debido a que DynamoDB no le permite modificar la clave primaria de un elemento, usted debe eliminar el elemento de correo electrónico único e insertar otro elemento con el nuevo correo electrónico.

aws dynamodb transact-write-items --client-request-token TRANSACTION3 --transact-items '[
  {
    "Update": {
      "TableName" : "User",
      "Key" : {"pk":{"S":"b201c1f2-238e-461f-88e6-0e606fbc3c51"}},
      "UpdateExpression":"SET email = :email",
      "ExpressionAttributeValues":{":email":{"S":"bobby@tables.com"}}
    }
  },
    {
    "Delete": {
      "TableName" : "User",
      "Key" : {"pk":{"S":"email#bobby.tables@gmail.com"}}
    }
  },
  {
    "Put": {
      "TableName" : "User",
      "ConditionExpression": "attribute_not_exists(pk)",
      "Item" : {
        "pk":{"S":"email#bobby@tables.com"}
       }
    }
}

Un escaneo muestra que se realizaron los cambios previstos.

aws dynamodb scan --table-name User
{
    "Count": 3,
    "Items": [
        {
            "userName": {
                "S": "btables"
            },
            "pk": {
                "S": "b201c1f2-238e-461f-88e6-0e606fbc3c51"
            },
            "fullName": {
                "S": "Bobby Tables"
            },
            "phoneNumber": {
                "S": "+1-202-555-0124"
            },
            "email": {
                "S": "bobby@tables.com"
            }
        },
        {
            "pk": {
                "S": "userName#btables"
            }
        },
        {
            "pk": {
                "S": "email#bobby@tables.com"
            }
        }
    ],
    "ScannedCount": 3,
    "ConsumedCapacity": null
}

Del mismo modo, cuando Bobby quiere borrarse a sí mismo del Internet, debe eliminar las tres filas relacionadas con su usuario.

aws dynamodb transact-write-items --client-request-token TRANSACTION4 --transact-items '[
  {
    "Delete": {
      "TableName" : "User",
      "Key" : {"pk":{"S":"b201c1f2-238e-461f-88e6-0e606fbc3c51"}}
    }
  },
  {
    "Delete": {
      "TableName" : "User",
      "Key" : {"pk":{"S":"userName#btables"}}
    }
  },
  {
    "Delete": {
      "TableName" : "User",
      "Key" : {"pk":{"S":"email#bobby@tables.com"}}
    }
  }
]'

Un escaneo final muestra que la tabla ahora está vacía:

aws dynamodb scan --table-name User
{
    "Count": 0,
    "Items": [],
    "ScannedCount": 0,
    "ConsumedCapacity": null
}

Conclusión

Este patrón puede resultarle útil si está migrando desde bases de datos relacionales y debe mantener restricciones de unicidad en DynamoDB. Debido a que una sola transacción puede modificar más de una tabla, es posible implementar este mismo patrón usando dos tablas. Mantenga sus «datos principales» en la tabla User y tenga una segunda tabla, que se utilice únicamente con el fin de garantizar la unicidad de ciertos atributos.

Intentemos mantener el concepto de diseño de tabla única siempre que sea posible en DynamoDB, pero si usar dos tablas para este patrón lo hace sentir más cómodo, ¡hágalo!

 

Este artículo fue traducido del Blog de AWS en Inglés.

 

 


Sobre los autores

Chad Tindel es un Arquitecto de Soluciones Especialista en DynamoDB con sede en la ciudad de Nueva York. Trabaja con grandes empresas para evaluar, diseñar e implementar soluciones basadas en DynamoDB. Antes de unirme a Amazon, ocupó puestos similares en Red Hat, Cloudera, MongoDB y Elastic.

 

 

 

 

 

Brett Hensley es parte del equipo de Arquitectos de Soluciones de AWS que se enfoca en SLG, que incluye entidades gubernamentales a nivel estatal, municipal y de condado junto con GovTechs, los diversos socios tecnológicos del gobierno. Antes de unirse a AWS, Brett ocupó varios puestos técnicos en empresas como Kimball Electronics, Maxim Group, BMG Columbia House y la filial de Hearst Health fdb (First DataBank). Después de haber trabajado en la industria durante más de 20 años apoyando en los sectores de manufactora, comercio minorista, salud y gobierno, Brett ayudar a sus clientes a navegar los diversos desafíos técnicos, culturales y de procedimiento para brindar un mejor servicio a sus negocios, clientes o electores.