Etapa 2: examinar o modelo de dados e os detalhes da implantação - Amazon DynamoDB

Etapa 2: examinar o modelo de dados e os detalhes da implantação

2.1: modelo de dados básico

Esta aplicação de exemplo destaca os seguintes conceitos de modelo de dados do DynamoDB:

  • Tabela: no DynamoDB, uma tabela é uma coleção de itens (ou seja, registros), e cada item é uma coleção de pares de nome-valor chamados atributos.

    Neste exemplo de Jogo da velha, o aplicativo armazena todos os dados do jogo em uma tabela, Games. O aplicativo cria um item na tabela por jogo e armazena todos os dados de jogos como atributos. Um jogo da velha pode ter até nove movimentações. Como as tabelas do DynamoDB não possuem um esquema em casos nos quais apenas a chave primária é o atributo obrigatório, a aplicação pode armazenar um número variável de atributos por item de jogo.

    A tabela Games possui uma chave primária simples composta de um atributo, GameId, do tipo string. O aplicativo atribui um ID exclusivo a cada jogo. Para obter mais informações sobre chaves primárias do DynamoDB, consulte Chave primária.

    Quando um usuário inicia um jogo da velha, convidando outro usuário para jogar, o aplicativo cria um novo item na tabela Games com atributos que armazenam metadados de jogos, como os seguintes:

    • HostId, o usuário que iniciou o jogo.

    • Opponent, o usuário que foi convidado para jogar.

    • O usuário que está na vez de jogar. O usuário que iniciou o jogo joga primeiro.

    • O usuário que usa o símbolo O no quadro. O usuário que inicia os jogos usa o símbolo O.

    Além disso, o aplicativo cria um atributo StatusDate concatenado, marcando o estado inicial do jogo como PENDING. A captura de tela a seguir mostra um item de exemplo como ele aparece no console do DynamoDB:

    Captura de tela do console da tabela de atributos.

    À medida que o jogo progride, o aplicativo adiciona um atributo à tabela para cada movimento do jogo. O nome do atributo é a posição no quadro, por exemplo TopLeft ou BottomRight. Por exemplo, um movimento pode ter um atributo TopLeft com o valor O, um atributo TopRight com o valor O e um atributo BottomRight com o valor X. O valor do atributo é O ou X, dependendo de qual usuário fez o movimento. Por exemplo, considere o quadro a seguir.

    Captura de tela mostrando um jogo da velha finalizado que terminou em um empate.
  • Atributos de valor concatenados: o atributo StatusDate ilustra um atributo de valor concatenado. Em essa abordagem, em vez de criar atributos separados para armazenar o status do jogo (PENDING, IN_PROGRESS e FINISHED) e a data (quando o último movimento foi feito), você pode combiná-los como um único atributo, por exemplo IN_PROGRESS_2014-04-30 10:20:32.

    Em seguida, o aplicativo usa o atributo StatusDate na criação de índices secundários, especificando StatusDate como uma chave de classificação para o índice. A vantagem de usar o atributo de valor concatenado StatusDate é melhor demonstrada nos índices discutidos a seguir.

  • Índices secundários globais: você pode usar a chave primária da tabela, GameId, para consultar a tabela com eficiência para encontrar um item do jogo. Para possibilitar a consulta de outros atributos que não sejam atributos da chave primária na tabela, o DynamoDB oferece suporte à criação de índices secundários. Neste aplicativo de exemplo, você cria os seguintes dois índices secundários:

    Captura de tela mostrando os índices secundários globais hostStatusDate e oppStatusDate criados no aplicativo de exemplo.
    • HostId-StatusDate-index. O índice tem HostId como chave de partição e StatusDate como chave de classificação. Você pode usar esse índice para consultar o HostId, por exemplo, para localizar jogos hospedados por um determinado usuário.

    • OpponentId-StatusDate-index. O índice tem OpponentId como chave de partição e StatusDate como chave de classificação. Você pode usar esse índice para consultar o Opponent, por exemplo, para localizar jogos nos quais um determinado usuário é o oponente.

    Esses índices são chamados de índices secundários globais porque a chave de partição nesses índices não é igual à chave de partição (GameId), usada na chave primária da tabela.

    Observe que ambos os índices especificam StatusDate como chave de classificação. Fazer isso permite o seguinte:

    • Você pode consultar usando o operador de comparação BEGINS_WITH. Por exemplo, você pode encontrar todos os jogos com o atributo IN_PROGRESS hospedados por um determinado usuário. Neste caso, o operador BEGINS_WITH verifica o valor StatusDate que começa com IN_PROGRESS.

    • O DynamoDB armazena os itens no índice em ordem classificada, por valor de chave de classificação. Portanto, se todos os prefixos de status forem os mesmos (por exemplo, IN_PROGRESS), o formato ISO usado para a parte de data terá itens classificados do mais antigo para o mais recente. Essa abordagem permite que determinadas consultas sejam executadas de forma eficiente, por exemplo, a seguinte:

      • Recupere até 10 dos jogos IN_PROGRESS mais recentes hospedados pelo usuário que está conectado. Para essa consulta, você especifica o índice HostId-StatusDate-index.

      • Recupere até 10 dos jogos IN_PROGRESS mais recentes nos quais o usuário conectado é o oponente. Para essa consulta, você especifica o índice OpponentId-StatusDate-index.

Para obter mais informações sobre índices secundários, consulte Melhorar o acesso aos dados com índices secundários no DynamoDB.

2.2: aplicação em ação (demonstração do código)

Este aplicativo tem duas páginas principais:

  • Página inicial – esta página oferece ao usuário um login simples, um botão CRIAR para criar um novo jogo da velha, uma lista de jogos em andamento, o histórico de jogos e todos os convites de jogo pendentes ativos.

    A página inicial não é atualizada automaticamente; você deve atualizar a página para atualizar as listas.

  • Página do jogo – Esta página mostra a grade do jogo da velha na qual os usuários jogam.

    O aplicativo atualiza a página de jogos automaticamente a cada segundo. O JavaScript no navegador chama o servidor web Python a cada segundo para consultar na tabela Games se os itens do jogo na tabela foram alterados. Se for o caso, o JavaScript aciona uma atualização da página para que o usuário veja o quadro atualizado.

Vamos ver em detalhes como o aplicativo funciona.

Home page (Página inicial)

Depois que o usuário fizer login, o aplicativo exibe as três listas de informações a seguir.

Captura de tela mostrando a página inicial do aplicativo com 3 listas: convites pendentes, jogos em andamento e histórico recente.
  • Convites: esta lista mostra até 10 convites mais recentes de outros usuários que estão aguardando aceitação pelo usuário que está conectado. Na captura de tela anterior, o user1 tem convites de user5 e de user2 pendentes.

  • Jogos em andamento: esta lista mostra até 10 jogos mais recentes que estão em andamento. Esses são os jogos que o usuário está ativamente jogando, que têm o status IN_PROGRESS. Na captura de tela, o user1 está jogando ativamente um jogo da velha com user3 e user4.

  • Histórico recente: esta lista mostra até 10 jogos mais recentes que o usuário terminou, os quais tem o status FINISHED. No jogo apresentado na captura de tela, o user1 jogou anteriormente com o user2. Para cada jogo concluído, a lista mostra o resultado do jogo.

No código, a função index (em application.py) faz as seguintes três chamadas para recuperar informações de status do jogo:

inviteGames = controller.getGameInvites(session["username"]) inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")

Cada uma dessas chamadas retorna uma lista de itens do DynamoDB que são encapsulados por objetos Game. É fácil extrair dados desses objetos na exibição. A função de índice passa essas listas de objetos para a exibição para renderizar o HTML.

return render_template("index.html", user=session["username"], invites=inviteGames, inprogress=inProgressGames, finished=finishedGames)

A aplicação Jogo da velha define a classe Game principalmente para armazenar os dados do jogo recuperados do DynamoDB. Essas funções retornam listas de objetos Game que permitem que você isole o resto da aplicação do código relacionado a itens do Amazon DynamoDB. Portanto, essas funções ajudam a separar o código do seu aplicativo dos detalhes da camada de armazenamento de dados.

O padrão de arquitetura descrito aqui também é chamado de padrão de interface do usuário MVC (controlador de visualização de modelo). Neste caso, as instâncias do objeto Game (representando os dados) são o modelo, e a página HTML é a exibição. O controlador é dividido em dois arquivos. O arquivo application.py tem o controlador para o framework Flask, e a lógica de negócios é isolada no arquivo gameController.py. Ou seja, a aplicação armazena tudo o que tem a ver com o DynamoDB SDK em seu próprio arquivo separado na pasta dynamodb.

Vamos analisar as três funções e como elas consultam a tabela Games usando índices secundários globais para recuperar dados relevantes.

Usar getGameInvites para obter a lista de convites de jogo pendentes

A função getGameInvites recupera a lista dos 10 convites pendentes mais recentes. Esses jogos foram criados pelos usuários, mas os oponentes não aceitaram os convites de jogo. Para esses jogos, o status permanece PENDING até que o oponente aceite o convite. Se o oponente recusar o convite, o aplicativo removerá o item correspondente da tabela.

A função especifica a consulta da seguinte forma:

  • Ela especifica o índice OpponentId-StatusDate-index para ser usado com os seguintes valores de chave de índice e operadores de comparação:

    • A chave de partição é OpponentId e usa a chave de índice user ID.

    • A chave de classificação é StatusDate e usa o operador de comparação e o valor de chave de índice beginswith="PENDING_".

    Você pode usar o índice OpponentId-StatusDate-index para recuperar jogos para os quais o usuário conectado é convidado – ou seja, nos quais o usuário conectado é o oponente.

  • A consulta limita o resultado a 10 itens.

gameInvitesIndex = self.cm.getGamesTable().query( Opponent__eq=user, StatusDate__beginswith="PENDING_", index="OpponentId-StatusDate-index", limit=10)

No índice, para cada OpponentId (a chave de partição), o DynamoDB mantém itens classificados por StatusDate (a chave de classificação). Portanto, os jogos que a consulta retorna serão os 10 jogos mais recentes.

Usar getGamesWithStatus para obter a lista de jogos com um status específico

Depois que um oponente aceita um convite de jogo, o status do jogo muda para IN_PROGRESS. Depois que o jogo for concluído, o status mudará para FINISHED.

As consultas para encontrar jogos que estão em andamento ou concluídos são as mesmas, exceto para o valor de status diferente. Portanto, o aplicativo define a função getGamesWithStatus, que usa o valor de status como um parâmetro.

inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")

A seção a seguir aborda os jogos em andamento, mas a mesma descrição também se aplica a jogos concluídos.

Uma lista de jogos em andamento para um determinado usuário inclui o seguinte:

  • Jogos em andamento hospedados pelo usuário

  • Jogos em andamento nos quais o usuário é o oponente

A função getGamesWithStatus executa as duas consultas seguintes, cada vez usando o índice secundário apropriado.

  • A função consulta a tabela Games usando o índice HostId-StatusDate-index. Para o índice, a consulta especifica valores de chave primária – tanto os valores de chave de partição (HostId) quanto os de chave de classificação (StatusDate), juntamente com operadores de comparação.

    hostGamesInProgress = self.cm.getGamesTable ().query(HostId__eq=user, StatusDate__beginswith=status, index="HostId-StatusDate-index", limit=10)

    Observe a sintaxe do Python para operadores de comparação:

    • HostId__eq=user especifica o operador de comparação de igualdade.

    • StatusDate__beginswith=status especifica o operador de comparação BEGINS_WITH.

  • A função consulta a tabela Games usando o índice OpponentId-StatusDate-index.

    oppGamesInProgress = self.cm.getGamesTable().query(Opponent__eq=user, StatusDate__beginswith=status, index="OpponentId-StatusDate-index", limit=10)
  • Em seguida, a função combina as duas listas, classifica, e para os primeiros itens de 0 a 10, cria uma lista dos objetos Game e retorna a lista para a função de chamada (ou seja, o índice).

    games = self.mergeQueries(hostGamesInProgress, oppGamesInProgress) return games

Página de jogos

A página de jogos é onde o usuário joga os jogos da velha. Ela mostra a grade do jogo junto com as informações relevantes do jogo. A captura de tela a seguir mostra um jogo de exemplo em andamento:

Captura de tela mostrando um jogo da velha em andamento.

O aplicativo exibe a página de jogos nas seguintes situações:

  • O usuário cria um jogo convidando outro usuário para jogar.

    Neste caso, a página mostra o usuário como host e o status do jogo como PENDING, aguardando o oponente aceitar.

  • O usuário aceita um dos convites pendentes na página inicial.

    Neste caso, a página mostra o usuário como o oponente e o status do jogo como IN_PROGRESS.

Uma seleção do usuário no quadro gera uma solicitação POST de formato para o aplicativo. Ou seja, o Flask chama a função selectSquare (em application.py) com os dados no formato HTML. Essa função, por sua vez, chama a função updateBoardAndTurn (em gameController.py) para atualizar o item de jogo da seguinte forma:

  • Ela adiciona um novo atributo específico ao movimento.

  • Ela atualiza o valor do atributo Turn para o usuário cuja vez é a próxima.

controller.updateBoardAndTurn(item, value, session["username"])

A função retorna verdadeiro se a atualização do item foi bem-sucedida; caso contrário, retorna falso. Observe o seguinte sobre a função updateBoardAndTurn:

  • A função chama a função update_item do SDK for Python para fazer um conjunto finito de atualizações em um item existente. A função é mapeada na operação UpdateItem no DynamoDB. Para obter mais informações, consulte UpdateItem.

    nota

    A diferença entre as operações UpdateItem e PutItem é que PutItem substitui o item inteiro. Para obter mais informações, consulte PutItem.

Para a chamada update_item, o código identifica o seguinte:

  • A chave primária da tabela Games (ou seja, ItemId).

    key = { "GameId" : { "S" : gameId } }
  • O novo atributo a ser adicionado, específico para o movimento do usuário atual, e seu valor (por exemplo, TopLeft="X").

    attributeUpdates = { position : { "Action" : "PUT", "Value" : { "S" : representation } } }
  • Condições que devem ser verdadeiras para a atualização acontecer:

    • O jogo deve estar em andamento. Ou seja, o valor do atributo StatusDate deve começar com IN_PROGRESS.

    • A vez atual deve ser de um usuário válido, conforme especificado pelo atributo Turn.

    • O quadrado que o usuário escolheu deve estar disponível. Ou seja, o atributo correspondente ao quadrado não deve existir.

    expectations = {"StatusDate" : {"AttributeValueList": [{"S" : "IN_PROGRESS_"}], "ComparisonOperator": "BEGINS_WITH"}, "Turn" : {"Value" : {"S" : current_player}}, position : {"Exists" : False}}

Agora, a função chama update_item para atualizar o item.

self.cm.db.update_item("Games", key=key, attribute_updates=attributeUpdates, expected=expectations)

Depois que a função retorna, as chamadas da função selectSquare são redirecionadas conforme mostrado no exemplo a seguir.

redirect("/game="+gameId)

Essa chamada faz com que o navegador seja atualizado. Como parte dessa atualização, o aplicativo verifica se o jogo terminou em uma vitória ou empate. Em caso afirmativo, o aplicativo atualizará o item de jogo adequadamente.