En el módulo anterior, definimos los patrones de acceso de la aplicación del juego. En este módulo, diseñamos la clave principal para la tabla de DynamoDB y habilitamos los patrones de acceso principales.

Tiempo para completar el módulo: 20 minutos


Cuando diseñe la clave principal para una tabla de DynamoDB, tenga en cuenta las siguientes prácticas recomendadas:

  • Comience con las diferentes entidades en su tabla. Si está almacenando múltiples tipos de datos diferentes en una sola tabla, como empleados, departamentos, clientes y pedidos, asegúrese de que su clave principal tenga una manera de identificar claramente cada entidad y habilitar acciones centrales en artículos individuales.
  • Use prefijos para distinguir entre tipos de entidades. El uso de prefijos para distinguir entre tipos de entidad puede evitar colisiones y ayudar en las consultas. Por ejemplo, si tiene tanto clientes como empleados en la misma tabla, la clave principal para un cliente podría ser CUSTOMER#<CUSTOMERID> y la clave principal para un empleado podría ser EMPLOYEE#<EMPLOYEEID>.
  • Concéntrese primero en acciones de un solo elemento y luego agregue acciones de varios elementos si es posible. Para una clave principal, es importante que pueda satisfacer las opciones de lectura y escritura en un solo elemento mediante las API de un solo elemento: GetItem, PutItem, UpdateItem y DeleteItem. También puede satisfacer sus patrones de lectura de elementos múltiples con la clave principal mediante el uso de Query. De lo contrario, puede agregar un índice secundario para manejar los casos de uso de la Query.

Con estas prácticas recomendadas en mente, diseñemos la clave principal para la tabla de la aplicación del juego y realicemos algunas acciones básicas.


  • Paso 1. Diseñar la clave primaria

    Consideremos las diferentes entidades, como se sugirió en la introducción anterior. En el juego, tenemos las siguientes entidades:

    • User
    • Game
    • UserGameMapping

    Un UserGameMapping es un registro que indica que un usuario se unió a una partida. Existe una relación de muchos a muchos entre User y el Game.

    Tener un mapeo de muchos a muchos suele ser una indicación de que desea satisfacer dos patrones de consulta y este juego no es una excepción. Tenemos un patrón de acceso que necesita encontrar a todos los usuarios que se han unido a un juego, así como otro patrón para encontrar todos los juegos que ha jugado un usuario.

    Si su modelo de datos tiene varias entidades con relaciones entre ellas, generalmente usa una clave primaria compuesta con valores HASH y RANGE. La clave primaria compuesta nos brinda la capacidad Query en la clave HASH para satisfacer uno de los patrones de consulta que necesitamos. En la documentación de DynamoDB, la clave de partición se llama HASH y la clave de clasificación se llama RANGE y en esta guía usamos la terminología de API de manera intercambiable y especialmente cuando discutimos el código o el formato de protocolo de cable JSON de DynamoDB.

    Las otras dos entidades de datos, User y Game, no tienen una propiedad natural para el valor RANGE porque los patrones de acceso en un User o Game son una búsqueda de valor clave. Debido a que se requiere un valor RANGE, podemos proporcionar un valor de relleno para la clave RANGE.

    Con esto en mente, usemos el siguiente patrón para los valores HASH y RANGE para cada tipo de entidad.

    Entidad HASH RANGE
    User USER#<USERNAME> #METADATA#<USERNAME>
    Game GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping GAME#<GAME_ID> USER#<USERNAME>

    Pasemos por la tabla anterior.

    Para la entidad User, el valor HASH es USER#<USERNAME>. Tenga en cuenta que utilizamos un prefijo para identificar la entidad y evitar posibles colisiones entre tipos de entidad.

    Para el valor RANGE en la entidad User, utilizamos un prefijo estático de #METADATA# seguido del valor USERNAME. Para el valor RANGE, es importante que tengamos un valor conocido, como USERNAME. Esto permite acciones de un solo elemento, como GetItem, PutItem y DeleteItem.

    Sin embargo, también queremos un valor RANGE con diferentes valores en diferentes entidades User para permitir una partición uniforme si utilizamos esta columna como clave HASH para un índice. Por esa razón, agregamos el USERNAME.

    La entidad Game tiene un diseño de clave principal que es similar al diseño de la entidad del Usuario. Utiliza un prefijo diferente (GAME#) y un GAME_ID en lugar de un USERNAME, pero los principios son los mismos.

    Finalmente, UserGameMapping usa la misma clave HASH que la entidad Game. Esto nos permite obtener no solo los metadatos de un Game sino también a todos los usuarios de un Game en una sola consulta. Luego usamos la entidad User para la clave RANGE en UserGameMapping para identificar qué usuario se unió a una partida específica.

    En el siguiente paso, creamos una tabla con este diseño de clave principal. 

  • Paso 2: Crear una tabla

    Ahora que diseñamos la clave principal, creemos una tabla.

    El código que descargó en el paso 3 del Módulo 1 incluye un script Python en el directorio scripts/ llamado create_table.py. El contenido del script Python sigue.

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.create_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            KeySchema=[
                {
                    "AttributeName": "PK",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "SK",
                    "KeyType": "RANGE"
                }
            ],
            ProvisionedThroughput={
                "ReadCapacityUnits": 1,
                "WriteCapacityUnits": 1
            }
        )
        print("Table created successfully.")
    except Exception as e:
        print("Could not create table. Error:")
        print(e)
    

    El script anterior utiliza la operación CreateTable con Boto 3, el SDK de AWS para Python. La operación declara dos definiciones de atributos, lo cuales son atributos escritos que se utilizarán en la clave primaria. Aunque DynamoDB no tiene esquemas, debe declarar los nombres y los tipos de atributos que se utilizan para las claves principales. Los atributos deben incluirse en cada elemento que se escribe en la tabla y, por lo tanto, deben especificarse a medida que crea una tabla.

    Debido a que almacenamos diferentes entidades en una sola tabla, no podemos usar nombres de atributos de clave primaria como UserId. El atributo significa algo diferente en función del tipo de entidad que se almacena. Por ejemplo, la clave principal para un usuario podría ser su USERNAME y la clave principal para un juego podría ser su GAMEID. En consecuencia, usamos nombres genéricos para los atributos, como PK (para la clave de partición) y SK (para la clave de clasificación).

    Después de configurar los atributos en el esquema clave, especificamos el rendimiento aprovisionado para la tabla. DynamoDB tiene dos modos de capacidad: aprovisionada y bajo demanda. En el modo de capacidad aprovisionada, especifica exactamente la cantidad de rendimiento de lectura y escritura que desea. Paga por esta capacidad, ya sea que la use o no.

    En el modo de capacidad bajo demanda de DynamoDB, puede pagar por solicitud. El costo por solicitud es un poco más alto que si usara el rendimiento aprovisionado por completo, pero no tiene que perder tiempo en la planificación de capacidad o preocuparse por las limitaciones al rendimiento. El modo bajo demanda funciona muy bien para cargas de trabajo con picos o impredecibles. En este laboratorio usamos el modo de capacidad aprovisionada porque se ajusta a la capa gratuita de DynamoDB.

    Para crear la tabla, ejecute el script de Python con el siguiente comando.

    python scripts/create_table.py

    El script debe presentar este mensaje: “Tabla creada con éxito”.

    En el siguiente paso, cargamos en masa algunos datos de ejemplo en la tabla. 

  • Paso 3: Carga masiva de datos en la tabla

    En este paso, cargamos en masa algunos datos en el DynamoDB que creamos en el paso anterior. Esto significa que en los pasos siguientes, tendremos datos de muestra para usar.

    En el directorio scripts/, encontrará un archivo llamado items.json. Este archivo contiene 835 elementos de ejemplo que se generaron aleatoriamente para este laboratorio. Estos elementos incluyen entidades de User, Game y UserGameMapping. Abra el archivo si desea ver algunos de los elementos de ejemplo.

    El directorio scripts/ también tiene un archivo llamado bulk_load_table.py que lee los elementos en el archivo items.json y los escribe en masa en la tabla de DynamoDB. El contenido de ese archivo sigue.

    import json
    
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('battle-royale')
    
    items = []
    
    with open('scripts/items.json', 'r') as f:
        for row in f:
            items.append(json.loads(row))
    
    with table.batch_writer() as batch:
        for item in items:
            batch.put_item(Item=item)
    

    En este script, en lugar de usar el cliente de bajo nivel en Boto 3, usamos un objeto Resource de nivel superior. Los objetos Resource proporcionan una interfaz más fácil para usar las API de AWS. El objeto Resource es útil en esta situación porque procesa nuestras solicitudes en lotes. La operación BatchWriteItem acepta hasta 25 elementos en una sola solicitud. El objeto de recursos maneja ese lote por nosotros en lugar de hacernos dividir nuestros datos en solicitudes de 25 elementos o menos.

    Ejecute el script bulk_load_table.py y cargue su tabla con datos mediante la ejecución del siguiente comando en el terminal.

    python scripts/bulk_load_table.py

    Puede asegurarse de que todos sus datos se hayan cargado en la tabla mediante la ejecución una operación Scan y la obtención del recuento.

    aws dynamodb scan \
     --table-name battle-royale \
     --select COUNT

    Esto debería mostrar los siguientes resultados.

    {
        "Count": 835, 
        "ScannedCount": 835, 
        "ConsumedCapacity": null
    }
    

    Debería ver un Count de 835, que indica que todos sus elementos se cargaron correctamente.

    En el siguiente paso, mostramos cómo recuperar múltiples tipos de entidades en una sola solicitud, lo que puede reducir el total de solicitudes de red que realiza en su aplicación y mejorar el rendimiento de la aplicación.

  • Paso 4: Recuperar múltiples tipos de entidad en una sola solicitud

    Como mencionamos en el módulo anterior, debe optimizar las tablas de DynamoDB para la cantidad de solicitudes que recibe. También mencionamos que DynamoDB no tiene las uniones que tiene una base de datos relacional. En su lugar, diseñe su tabla para permitir un comportamiento similar a una unión en sus solicitudes.

    En este paso, recuperamos múltiples tipos de entidad en una sola solicitud. En el juego, es posible que queramos obtener detalles sobre una sesión de juego. Estos detalles incluyen información sobre el juego en sí, como la hora en que comenzó, la hora en que terminó, quién lo colocó y detalles sobre los usuarios que jugaron en el juego.

    Esta solicitud abarca dos tipos de entidad: la entidad Game y la entidad UserGameMapping. Sin embargo, esto no significa que debamos realizar varias solicitudes.

    En el código que descargó, hay un script fetch_game_and_players.py en el directorio application/. Este script muestra cómo puede estructurar su código para recuperar la entidad Game y la entidad UserGameMapping para el juego en una sola solicitud.

    El siguiente código compone el script fetch_game_and_players.py.

    import boto3
    
    from entities import Game, UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "3d4285f0-e52b-401a-a59b-112b38c4a26b"
    
    
    def fetch_game_and_users(game_id):
        resp = dynamodb.query(
            TableName='battle-royale',
            KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :users",
            ExpressionAttributeValues={
                ":pk": { "S": "GAME#{}".format(game_id) },
                ":metadata": { "S": "#METADATA#{}".format(game_id) },
                ":users": { "S": "USER$" },
            },
            ScanIndexForward=True
        )
    
        game = Game(resp['Items'][0])
        game.users = [UserGameMapping(item) for item in resp['Items'][1:]]
    
        return game
    
    
    game = fetch_game_and_users(GAME_ID)
    
    print(game)
    for user in game.users:
        print(user)
    

    Al comienzo de este script, importamos la biblioteca Boto 3 y algunas clases simples para representar los objetos en nuestro código de aplicación. Puede consultar las definiciones para esas entidades en el archivo application/entities.py.

    El verdadero trabajo del script ocurre en la función fetch_game_and_users que se define en el módulo. Esto es similar a una función que definiría en su aplicación para ser utilizada por cualquier punto de enlace que necesite estos datos.

    La función fetch_game_and_users realiza algunas acciones. Primero, realiza una solicitud Query a DynamoDB. Esta Query usa una PK de GAME#<GameId>. Luego, solicita cualquier entidad donde la clave de clasificación esté entre #METADATA#<GameId> y USER$. Esto toma la entidad Game, cuya clave de clasificación es #METADATA#<GameId> y todas las entidades UserGameMappings, cuyas claves comienzan con USER#. Las claves de clasificación del tipo de cadena se ordenan por códigos de caracteres ASCII. El signo de dólar ($) viene directamente después del signo de libra (#) en ASCII, por lo que esto asegura que obtendremos todas las asignaciones en la entidad UserGameMapping.

    Cuando recibimos una respuesta, reunimos nuestras entidades de datos en objetos conocidos por nuestra aplicación. Sabemos que la primera entidad devuelta es la entidad Game, por lo que creamos un objeto Game a partir de la entidad. Para las entidades restantes, creamos un objeto UserGameMapping para cada entidad y luego asociamos la matriz de usuarios al objeto Game.

    El final del script muestra el uso de la función e imprime los objetos resultantes. Puede ejecutar el script en su terminal con el siguiente comando.

    python application/fetch_game_and_players.py

    El script debe imprimir el objeto Game y todos los objetos UserGameMapping en la consola.

    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- branchmichael>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- deanmcclure>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emccoy>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emma83>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- iherrera>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- jeremyjohnson>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- lisabaker>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- maryharris>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- mayrebecca>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- meghanhernandez>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- nruiz>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- pboyd>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- richardbowman>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- roberthill>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- robertwood>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- victoriapatrick>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- waltervargas>
    

    Este script muestra cómo puede modelar su tabla y escribir sus consultas para recuperar múltiples tipos de entidad en una sola solicitud de DynamoDB. En una base de datos relacional, utiliza combinaciones para recuperar múltiples tipos de entidades de diferentes tablas en una sola solicitud. Con DynamoDB, modela específicamente sus datos, de modo que las entidades a las que deben acceder al mismo tiempo se encuentren una al lado de la otra en una sola tabla. Este enfoque reemplaza la necesidad de uniones en una base de datos relacional típica y mantiene su aplicación de alto rendimiento a medida que escala.


    En este módulo, diseñamos una clave primaria y creamos una tabla. Luego, cargamos datos en la tabla en masa y vimos cómo consultar múltiples tipos de entidades en una sola solicitud.

    Con el diseño actual de la clave principal, podemos satisfacer los siguientes patrones de acceso:

    • Crear perfil de usuario (escritura)
    • Actualizar el perfil de usuario (Escribir)
    • Obtener perfil de usuario (lectura)
    • Crear juego (escritura)
    • Ver juego (lectura)
    • Unirse al juego para un usuario (escritura)
    • Comenzar juego (escritura)
    • Actualizar juego para un usuario (escritura)
    • Actualizar juego (escritura)

    En el siguiente módulo, agregaremos un índice secundario y aprenderemos sobre la técnica del índice disperso. Los índices secundarios le permiten admitir patrones de acceso adicionales en su tabla de DynamoDB.