No módulo anterior, definimos os padrões de acesso do aplicativo de jogo. Neste módulo, projetamos a chave primária da tabela do DynamoDB e habilitamos os padrões de acesso principais.

Tempo de conclusão do módulo: 20 minutos


Ao projetar a chave primária de uma tabela do DynamoDB, mantenha as seguintes melhores práticas em mente:

  • Comece com as diferentes entidades em sua tabela. Se você estiver armazenando vários tipos diferentes de dados em uma única tabela (como funcionários, departamentos, clientes e pedidos), certifique-se de que sua chave primária tenha uma maneira de identificar distintivamente cada entidade e habilitar as ações principais em itens individuais.
  • Use prefixos para distinguir entre os tipos de entidades. O uso de prefixos para distinguir entre tipos de entidades pode evitar colisões e auxiliar na realização das consultas. Por exemplo, se você tem clientes e funcionários na mesma tabela, a chave primária de um cliente poderia ser CUSTOMER# <CUSTOMERID> e a chave primária de um funcionário poderia ser EMPLOYEE# <EMPLOYEEID>.
  • Concentre-se primeiro em ações de item único, em seguida adiciona ações de vários itens, se possível. Para uma chave primária, é importante satisfazer as opções de leitura e gravação em um único item usando as APIs de item único: GetItem, PutItem, UpdateItem e DeleteItem. Você também pode ser capaz de satisfazer seus padrões de leitura de múltiplos itens com a chave primária usando Query. Caso contrário, você pode adicionar um índice secundário para gerenciar os casos de uso de Query.

Com essas melhores práticas em mente, vamos projetar a chave primária para a tabela do aplicativo de jogo e executar algumas ações básicas.


  • Etapa 1. Projete a chave primária

    Vamos considerar as diferentes entidades, como sugerido na introdução acima. No jogo, temos as seguintes entidades:

    • User
    • Game
    • UserGameMapping

    Um UserGameMapping é um registro que indica que um usuário ingressou em um jogo. Há um relacionamento muitos-para-muitos entre User e Game.

    Ter um mapeamento muitos-para-muitos normalmente é um indicativo de que você quer satisfazer dois padrões de Query, e este jogo não é uma exceção. Temos um padrão de acesso que precisa encontrar todos os usuários que ingressaram em um jogo, bem como outro padrão para encontrar todos os jogos que um usuário jogou.

    Se o seu modelo de dados tem múltiplas entidades com relacionamentos entre elas, você usa geralmente uma chave primária composta com os valores de HASH e RANGE. A chave primária composta nos oferece a capacidade de usar Query na chave HASH para satisfazer um dos padrões de consulta de que precisamos. Na documentação do DynamoDB, a chave da partição é chamada de HASH e a chave de classificação é chamada de RANGE. Neste guia, usamos a intercambialidade da terminologia da API e especialmente quando discutimos o código ou o formato de protocolo cabeado JSON do DynamoDB.

    As outras duas entidades de dados – User e Game – não têm uma propriedade natural para o valor de RANGE, pois os padrões de acesso em um User ou Game são uma procura de chave-valor. Como um valor de RANGE é necessário, podemos fornecer um valor de preenchimento para a chave RANGE.

    Com isso em mente, vamos usar o seguinte padrão para os valores de HASH e RANGE para cada tipo de entidade.

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

    Vamos analisar a tabela acima.

    Para a entidade User, o valor de HASH é USER#<USERNAME>. Observe que estamos usando um prefixo para identificar a entidade e evitar quaisquer colisões possíveis entre os tipos de entidade.

    Para o valor de RANGE na entidade User, estamos usando um prefixo estático de #METADATA# seguido do valor de USERNAME. Para o valor de RANGE, é importante que tenhamos um valor conhecido, como USERNAME. Isso permite ações de item único, como GetItem, PutItem e DeleteItem.

    No entanto, também queremos um valor de RANGE com diferentes valores em diferentes entidades User para habilitar o particionamento uniforme se usarmos essa coluna como uma chave HASH para um índice. Por esse motivo, anexamos o USERNAME.

    A entidade Game tem um design de chave primária similar ao design da entidade User. Ela usa um prefixo diferente (GAME#) e um GAME_ID em vez de um USERNAME, mas os princípios são os mesmos.

    Por fim, o UserGameMapping usa a mesma chave HASH como a entidade Game. Isso nos permite buscar não só os metadados de um Game, mas também todos os usuários em um Game em uma consulta única. Então usamos a entidade User para a chave RANGE no UserGameMapping para identificar qual usuário ingressou em determinado jogo.

    Na etapa seguinte, criaremos uma tabela com esse projeto de chave primária. 

  • Etapa 2: Crie um fluxo

    Agora que projetamos a chave primária, vamos criar uma tabela.

    O código que você baixou na Etapa 3 do Módulo 1 inclui um script Python no diretório scripts/ chamado de create_table.py. Veja o conteúdo do script Python a seguir.

    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)
    

    O script precedente usa a operação CreateTable usando Boto 3, o AWS SDK para Python. A operação declara duas definições de atributos, que são atributos tipificados a serem usados na chave primária. Embora o DynamoDB não tenha esquema, você precisa declarar os nomes e os tipos de atributos que são usadas para chaves primárias. Os atributos precisam ser incluídos em cada item que seja gravado na tabela e, assim, devem ser especificados à medida que você está criando uma tabela.

    Como estamos armazenando entidades diferentes em uma única tabela, não podemos usar nomes de atributos de chaves, como UserId. O atributo significa algo diferente com base no tipo de entidade sendo armazenado. Por exemplo, a chave primária de um usuário pode ser o USERNAME e a chave primária de um jogo pode ser o GAMEID. De maneira similar, usamos nomes genéricos para os atributos, como PK (para chave de partição) e SK (para chave de classificação).

    Depois de configurar os atributos no esquema da chave, especificamos o throughput provisionado para a tabela. O DynamoDB tem dois modos de capacidade: provisionado e sob demanda. No modo de capacidade provisionado, você especifica exatamente a quantidade de throughput de leitura e gravação que deseja. Você paga por essa capacidade, quer use-a ou não.

    No modo de capacidade sob demanda do DynamoDB, você pode pagar por solicitação. O custo por solicitação é ligeiramente mais elevado do que se você usasse o throughput provisionado totalmente, mas você não tem que perder tempo fazendo planejamento de capacidade ou se preocupando com ficar com o uso limitado. O modo sob demanda funciona muito bem para cargas de trabalho com muitos picos ou imprevisíveis. Estamos usando o modo de capacidade provisionada neste laboratório porque ele se adéqua ao nível gratuito do DynamoDB.

    Para criar a tabela, execute o script do Python com o comando a seguir.

    python scripts/create_table.py

    O script deve retornar esta mensagem: “Table created successfully.” (Tabela criada com êxito.).

    Na próxima etapa, carregaremos alguns dados de exemplo em lote na tabela. 

  • Etapa 3: Carregue dados em lote na tabela

    Nesta etapa, carregamos alguns dados em lote para a tabela do DynamoDB que criamos na etapa anterior. Isso significa que, nas etapas posteriores, teremos dados de amostra para usar.

    No diretório scripts/, você encontrará um arquivo chamado items.json. Esse arquivo contém 835 itens de exemplo que foram gerados aleatoriamente para este laboratório. Esses itens incluem as entidades User, Game e UserGameMapping. Abra o arquivo se quiser ver alguns dos itens de exemplo.

    O diretório scripts/ também tem um arquivo chamado bulk_load_table.py que lê os itens no arquivo items.json e os grava em lote na tabela do DynamoDB. Este é o conteúdo do arquivo:

    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)
    

    Neste script, em vez de usarmos o cliente de baixo nível no Boto 3, usamos um objeto Resource de nível mais elevado. Os objetos Resource fornecem uma interface mais fácil para usar as APIs da AWS. O objeto Resource é útil nessa situação porque ele agrupa nossas solicitações em lotes. A operação BatchWriteItem aceita até 25 itens em uma única solicitação. O objeto Resource gerencia esse agrupamento em lote para nós em vez de nos fazer dividir nossos dados em solicitações de 25 itens ou menos.

    Execute o script bulk_load_table.py e carregue sua tabela com dados executando o comando a seguir no terminal.

    python scripts/bulk_load_table.py

    Você pode garantir que todos os seus dados sejam carregados para a tabela executando uma operação Scan e obtendo a contagem.

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

    Os resultados a seguir devem ser exibidos.

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

    O valor de Count deve ser 835, indicando que todos os seus itens foram carregados com êxito.

    Na etapa seguinte, mostramos como recuperar vários tipos de entidades em uma única solicitação, que pode reduzir o total de solicitações de rede que você faz em seu aplicativo e aprimorar a performance dele.

  • Etapa 4: Recupere os vários tipos de entidades em uma única solicitação

    Como dissemos no módulo anterior, você deve otimizar as tabelas do DynamoDB para o número de solicitações que recebe. Também mencionamos que o DynamoDB não tem as uniões que um banco de dados relacional tem. Em vez disso, você projeta sua tabela para permitir um comportamento similar à junção em suas solicitações.

    Nesta etapa, recuperamos os vários tipos de entidades em uma única solicitação No jogo, podemos querer buscar detalhes sobre uma sessão de jogo. Esses detalhes incluem informações sobre o próprio jogo como o horário em que iniciou, o horário em que terminou e os detalhes sobre os usuários que jogaram no jogo.

    Essa solicitação se estende por dois tipos de entidade: a entidade Game e a entidade UserGameMapping. Contudo, isso não significa que precisamos fazer várias solicitações.

    No código que você baixou, há um arquivo fetch_game_and_players.py no diretório application/. Esse script mostra como você pode estruturar seu código para recuperar a entidade Game e a entidade UserGameMapping referentes ao jogo em uma única solicitação.

    O código a seguir compõe o 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)
    

    No início desse script, importamos a biblioteca Boto 3 e algumas classes simples para representar os objetos no código do nosso aplicativo. Podemos ver as definições dessas entidades no arquivo application/entities.py.

    O trabalho real do script ocorre na função fetch_game_and_uses que está definida no módulo. Isso é similar a uma função que você definiria em seu aplicativo para ser usada por quaisquer endpoints que precisassem desses dados.

    A função fetch_game_and_users faz algumas coisas. Primeiro, ela faz uma solicitação Query ao DynamoDB. Essa Query usa uma PK de GAME#<GameId>. Em seguida, ela solicita quaisquer entidades em que a chave de classificação esteja entre #METADATA#<GameId> e USER$. Essa ação toma a entidade Game, cuja chave de classificação é #METADATA#<GameId> e todas as entidades UserGameMappings, cujas chaves começam com USER#. As chaves de classificação do tipo string são classificadas pelos códigos de caracteres ASCII. O cifrão ($) vem diretamente após o sustenido (#) no ASCII, então isso garante que teremos todos os mapeamentos na entidade UserGameMapping.

    Quando recebemos uma resposta, montamos nossas entidades de dados em objetos conhecidos por nosso aplicativo. Sabemos que a primeira entidade retornada é a entidade Game, então criamos um objeto Game a partir da entidade. Para as entidades restantes, criamos um objeto UserGameMapping para cada entidade e em seguida anexamos a matriz de usuários ao objeto Game.

    O fim do script mostra o uso da função e imprime os objetos resultantes. Você pode executar o script com o seguinte comando no terminal:

    python application/fetch_game_and_players.py

    O script deve imprimir o objeto Game e todos os objetos UserGameMapping no console.

    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>
    

    Esse script mostra como você pode modelar sua tabela e gravar suas consultas para recuperar vários tipos de entidades em uma única solicitação do DynamoDB. Em um banco de dados relacional, você usa uniões para recuperar múltiplos tipos de entidades de diferentes tabelas em uma única solicitação. Com o DynamoDB você modela especificamente seus dados, de modo que as entidades que você deve acessar em conjunto ficam localizadas perto umas das outras em uma única tabela. Essa abordagem substitui a necessidade de uniões em um banco de dados relacional típico e mantém seu aplicativo com alta performance à medida que você o expande.


    Neste módulo, projetamos uma chave primária e criamos uma tabela. Em seguida, carregamos dados em lote na tabela e vimos como consultar vários tipos de entidades em uma única solicitação.

    Com o projeto atual da chave primária, podemos satisfazer os seguintes padrões de acesso:

    • Criar perfil do usuário (Gravação)
    • Atualizar perfil do usuário (Gravação)
    • Obter perfil do usuário (Leitura)
    • Criar jogo (Gravação)
    • Visualizar jogo (Leitura)
    • Participar do jogo de um usuário (Gravação)
    • Iniciar jogo(Gravação)
    • Atualizar jogo para o usuário (Gravação)
    • Atualizar jogo (Gravação)

    No próximo módulo, adicionaremos um índice secundário e aprenderemos sobre a técnica de índice esparso. Os índices secundários permitem dar suporte a outros padrões de acesso em sua tabela do DynamoDB.