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 comoPENDING
. A captura de tela a seguir mostra um item de exemplo como ele aparece no console do DynamoDB:À 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
ouBottomRight
. Por exemplo, um movimento pode ter um atributoTopLeft
com o valorO
, um atributoTopRight
com o valorO
e um atributoBottomRight
com o valorX
. O valor do atributo éO
ouX
, dependendo de qual usuário fez o movimento. Por exemplo, considere o quadro a seguir. -
-
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
eFINISHED
) e a data (quando o último movimento foi feito), você pode combiná-los como um único atributo, por exemploIN_PROGRESS_2014-04-30 10:20:32
.Em seguida, o aplicativo usa o atributo
StatusDate
na criação de índices secundários, especificandoStatusDate
como uma chave de classificação para o índice. A vantagem de usar o atributo de valor concatenadoStatusDate
é 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:-
HostId-StatusDate-index. O índice tem
HostId
como chave de partição eStatusDate
como chave de classificação. Você pode usar esse índice para consultar oHostId
, por exemplo, para localizar jogos hospedados por um determinado usuário. -
OpponentId-StatusDate-index. O índice tem
OpponentId
como chave de partição eStatusDate
como chave de classificação. Você pode usar esse índice para consultar oOpponent
, 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 atributoIN_PROGRESS
hospedados por um determinado usuário. Neste caso, o operadorBEGINS_WITH
verifica o valorStatusDate
que começa comIN_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 índiceHostId-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 índiceOpponentId-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.
-
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 índicebeginswith="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 índiceHostId-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çãoBEGINS_WITH
.
-
-
A função consulta a tabela
Games
usando o índiceOpponentId-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:
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çãoUpdateItem
no DynamoDB. Para obter mais informações, consulte UpdateItem.nota
A diferença entre as operações
UpdateItem
ePutItem
é quePutItem
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 comIN_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.