Paso 2: examinar el modelo de datos y los detalles de implementación
2.1: modelo de datos básicos
En este ejemplo de aplicación se resaltan los siguientes conceptos del modelo de datos de DynamoDB:
-
Tabla: en DynamoDB, una tabla es una colección de elementos (es decir, de registros) y cada elemento es una colección de pares de nombre-valor denominados atributos.
En este ejemplo del juego de tres en raya, la aplicación almacena todos los datos de las partidas en una tabla,
Games
. La aplicación crea un elemento en la tabla por cada partida y almacena todos los datos de las partidas como atributos. Una partida de tres en raya puede incluir hasta nueve jugadas o movimientos. Dado que las tablas de DynamoDB no tienen un esquema cuando el único atributo obligatorio es la clave principal, la aplicación puede almacenar un número variable de atributos por cada elemento del juego.La tabla
Games
cuenta con una clave principal sencilla que consta de un solo atributo,GameId
, de tipo String. La aplicación asigna identificador exclusivo a cada partida. Para obtener más información sobre claves principales en DynamoDB, consulte Clave principal.Cuando un usuario inicia una partida de tres en raya invitando a otro usuario a jugar, la aplicación crea un nuevo elemento en la tabla
Games
con atributos que almacenan los metadatos de la partida, tales como los siguientes:-
HostId
, usuario que inició la partida. -
Opponent
, usuario al que se invitó a jugar. -
Usuario al que le toca jugar. El usuario que inicia la partida juega primero.
-
El usuario que utiliza el símbolo O en el tablero. El usuario que inicia las partidas utiliza el símbolo O.
Además, la aplicación crea un atributo
StatusDate
concatenado que marca el estado inicial de la partida comoPENDING
. En la siguiente captura de pantalla se muestra un ejemplo de elemento tal y como aparece en la consola de DynamoDB:A medida que la partida avanza, la aplicación agrega un atributo a la tabla por cada jugada. El nombre del atributo es la posición en el tablero; por ejemplo,
TopLeft
oBottomRight
. Por ejemplo, una jugada puede tener el atributoTopLeft
con el valorO
, un atributoTopRight
con el valorO
y un atributoBottomRight
con el valorX
. El valor del atributo puede serO
oX
, según cuál sea el usuario que ha realizado la jugada. Por ejemplo, fíjese en el tablero siguiente. -
-
Atributos de valores concatenados: el atributo
StatusDate
ilustra un valor de atributo concatenado. En este enfoque, en lugar de crear atributos separados para almacenar el estado de la partida (PENDING
,IN_PROGRESS
yFINISHED
) y la fecha (cuándo se realizó la última jugada), se combinan como un solo atributo; por ejemploIN_PROGRESS_2014-04-30 10:20:32
.A continuación, la aplicación utiliza el atributo
StatusDate
para crear índices secundarios especificandoStatusDate
como clave de ordenación del índice. El beneficio de utilizar el atributo de valores concatenadosStatusDate
se ilustra mejor en la explicación sobre índices que encontrará más adelante. -
Índices secundarios globales: puede utilizar la clave principal de la tabla,
GameId
, para consultar esa tabla de manera eficiente con el fin de encontrar un elemento del juego. Para consultar la tabla para hallar otros atributos distintos de los de clave principal, DynamoDB admite la creación de índices secundarios. En este ejemplo de aplicación, se crean los dos índices secundarios siguientes:-
HostId-StatusDate-index. Este índice tiene
HostId
como clave de partición yStatusDate
como clave de ordenación. Puede utilizar este índice para realizar una consulta sobreHostId
, por ejemplo, para encontrar las partidas organizadas por un usuario concreto. -
OpponentId-StatusDate-index. Este índice tiene
OpponentId
como clave de partición yStatusDate
como clave de ordenación. Puede utilizar este índice para realizar una consulta sobreOpponent
, por ejemplo, para encontrar las partidas en las que un usuario determinado ha sido el contrincante.
Estos índices se denominan índices secundarios globales porque su clave de partición no es la misma (
GameId
) que se usó en la clave principal de la tabla.Tenga en cuenta que en los dos índices se especifica
StatusDate
como clave de ordenación. Al hacerlo, se habilita lo siguiente:-
Puede realizar consultas utilizando el operador de comparación
BEGINS_WITH
. Por ejemplo, puede buscar todas las partidas que tienen el atributoIN_PROGRESS
y que ha organizado un usuario determinado. En este caso, el operadorBEGINS_WITH
comprueba los valores deStatusDate
que comienzan porIN_PROGRESS
. -
DynamoDB almacena los elementos en el índice de forma secuencial, según el valor de la clave de ordenación. Así pues, si todos los prefijos de estado son iguales (por ejemplo,
IN_PROGRESS
), el formato ISO utilizado para la parte de la fecha hará que los elementos se ordenen por orden de antigüedad descendente. Este enfoque permite realizar con eficacia algunas consultas, tales como las siguientes:-
Recuperar como máximo las diez partidas
IN_PROGRESS
más recientes organizadas por el usuario que ha iniciado sesión. Para esta consulta, se especifica el índiceHostId-StatusDate-index
. -
Recuperar como máximo las diez partidas
IN_PROGRESS
más recientes en las que el usuario que ha iniciado sesión sea el contrincante. Para esta consulta, se especifica el índiceOpponentId-StatusDate-index
.
-
-
Para obtener más información acerca de los índices secundarios, consulte Mejora del acceso con índices secundarios en DynamoDB.
2.2: aplicación en acción (guía del código)
Esta aplicación tiene dos páginas principales:
-
Página de inicio: en esta página se proporciona al usuario un inicio de sesión sencillo, un botón CREATE para crear una nueva partida de tres en raya, una lista de partidas en curso, el historial de partidas y todas las invitaciones pendientes para jugar.
La página de inicio no se actualiza automáticamente; debe actualizarla para renovar la información de las listas.
-
Página de la partida: muestra la cuadrícula de tres en raya en la que juegan los usuarios.
La aplicación actualiza la página de la partida automáticamente cada segundo. El código JavaScript del navegador llama al servidor web Python cada segundo para consultar la tabla Games y saber si los elementos de partidas contenidos en la tabla han cambiado. En caso afirmativo, JavaScript activa una actualización de la página para que el usuario vea el tablero con la información más reciente.
Vamos a estudiar en detalle el funcionamiento de la aplicación.
Página de inicio
Cuando el usuario inicia sesión, la aplicación muestra las tres listas de información siguientes.
-
Invitaciones: esta lista muestra como máximo las diez invitaciones más recientes de otros usuarios que el usuario que ha iniciado sesión todavía no ha aceptado. En la captura de pantalla anterior, el usuario user1 tiene invitaciones pendientes de los usuarios user2 y user5.
-
Juegos en progreso: esta lista muestra como máximo las 10 partidas más recientes que están en curso. Se trata de partidas en las que el usuario está jugando activamente y cuyo estado es
IN_PROGRESS
. En la captura de pantalla, el usuario user1 está jugando activamente una partida de tres en raya contra los usuarios user3 y user4. -
Historia reciente: esta lista muestra como máximo las 10 partidas más recientes que el usuario ha terminado, cuyo estado es
FINISHED
. En el juego que aparece en la captura de pantalla, el usuario user1 ha jugado anteriormente contra el usuario user2. En la lista se muestra el resultado de cada partida completada.
En el código, la función index
(en application.py
) realiza las tres llamadas siguientes para recuperar la información de estado de las partidas:
inviteGames = controller.getGameInvites(session["username"]) inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")
Cada una de estas llamadas devuelve una lista de elementos de DynamoDB que están integrados en los objetos Game
. Resulta fácil extraer datos de estos objetos en la vista. La función de índice transmite estas listas de objetos a la vista para representar el código HTML.
return render_template("index.html", user=session["username"], invites=inviteGames, inprogress=inProgressGames, finished=finishedGames)
La aplicación del juego de Tic-Tac-Toe (Tres en raya) define la clase Game
principalmente para almacenar los datos de los juegos recuperados de DynamoDB. Estas funciones devuelven listas de objetos Game
que le permiten aislar el resto de la aplicación del código relacionado con los elementos de Amazon DynamoDB. Por lo tanto, estas funciones le ayudan a desacoplar el código de aplicación de los detalles de la capa de almacenamiento de datos.
El patrón de arquitectura descrito aquí también se denomina patrón de interfaz de usuario (IU) de tipo modelo-vista-controlador (MVC). En este caso, las instancias de objetos Game
(que representan datos) son el modelo y la página HTML es la vista. El controlador se divide en dos archivos. El archivo application.py
contiene la lógica del controlador correspondiente al marco de trabajo Flask, mientras que la lógica empresarial se aísla en el archivo gameController.py
. Es decir, la aplicación guarda todo lo que se refiere al SDK de DynamoDB en su propio archivo independiente dentro de la carpeta dynamodb
.
Vamos a revisar las tres funciones para entender cómo consultan la tabla Games y utilizan índices secundarios globales para recuperar los datos pertinentes.
Uso de getGameInvites para obtener la lista de invitaciones de juego pendientes
La función getGameInvites
recupera la lista de las diez invitaciones pendientes más recientes. Hay usuarios que han creado estas partidas, pero los contrincantes no han aceptado las invitaciones para jugar. En estas partidas, el estado sigue siendo PENDING
hasta que el contrincante acepta la invitación. Si el contrincante rechaza la invitación, la aplicación eliminará el elemento correspondiente de la tabla.
La función especifica la consulta de la siguiente manera:
-
Especifica el índice
OpponentId-StatusDate-index
que se debe usar con los siguientes valores de clave de índice y operadores de comparación:-
La clave de partición es
OpponentId
y acepta la clave de índice
.user ID
-
La clave de ordenación es
StatusDate
y acepta el operador de comparación y el valor de clave de índicebeginswith="PENDING_"
.
El índice
OpponentId-StatusDate-index
se utiliza para recuperar partidas a las que se ha invitado a jugar al usuario que ha iniciado sesión; es decir, en las que el usuario que ha iniciado sesión es el contrincante. -
-
La consulta limita el resultado a diez elementos.
gameInvitesIndex = self.cm.getGamesTable().query( Opponent__eq=user, StatusDate__beginswith="PENDING_", index="OpponentId-StatusDate-index", limit=10)
En el índice, por cada OpponentId
(clave de partición) DynamoDB ordena los elementos según StatusDate
(clave de ordenación). Por lo tanto, las partidas que devuelve la consulta son las diez más recientes.
Uso de getGamesWithStatus para obtener la lista de juegos con un estado determinado
Una vez que un contrincante ha aceptado una invitación a jugar, el estado de la partida cambia a IN_PROGRESS
. Cuando la partida finaliza, el estado cambia a FINISHED
.
Las consultas para buscar las partidas que se encuentran en curso o que ya han finalizado son iguales salvo por el valor de estado, que varía. Por consiguiente, la aplicación define la función getGamesWithStatus
, que acepta el valor de estado como parámetro.
inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")
En la sección siguiente se explican las partidas en curso, pero la misma descripción es aplicable también a las partidas finalizadas.
Una lista de partidas en curso de un usuario determinado incluye los dos tipos siguientes:
-
Las partidas en curso organizadas por el usuario
-
Las partidas en curso en las que el usuario es el contrincante
La función getGamesWithStatus
ejecuta las dos consultas siguientes, utilizando en cada caso el índice secundario apropiado.
-
La función consulta la tabla
Games
mediante el índiceHostId-StatusDate-index
. Para el índice, la consulta especifica los valores de clave principal; es decir, los valores tanto de la clave de partición (HostId
) como de la clave de ordenación (StatusDate
), así como los operadores de comparación.hostGamesInProgress = self.cm.getGamesTable ().query(HostId__eq=user, StatusDate__beginswith=status, index="HostId-StatusDate-index", limit=10)
Fíjese en la sintaxis de Python para los operadores de comparación:
-
HostId__eq=user
especifica el operador de comparación de igualdad. -
StatusDate__beginswith=status
especifica el operador de comparaciónBEGINS_WITH
.
-
-
La función consulta la tabla
Games
mediante el índiceOpponentId-StatusDate-index
.oppGamesInProgress = self.cm.getGamesTable().query(Opponent__eq=user, StatusDate__beginswith=status, index="OpponentId-StatusDate-index", limit=10)
-
A continuación, la función combina ambas listas, las ordena, crea una lista de entre 0 y 10 elementos que contiene los primeros objetos
Game
y se la devuelve a la función de llamada (es decir, al índice).games = self.mergeQueries(hostGamesInProgress, oppGamesInProgress) return games
Página del juego
La página de la partida es donde el usuario juega las partidas de tres en raya. Muestra la cuadrícula del juego, además de la información pertinente sobre el juego. En la siguiente captura de pantalla se muestra un ejemplo de partida en curso:
La aplicación muestra la página de la partida en las siguientes situaciones:
-
Cuando el usuario ha creado una partida e invitado a otro usuario a jugar.
En este caso, la página muestra al usuario como anfitrión y el estado de la partida
PENDING
, mientras espera a que el contrincante acepte. -
Cuando el usuario ha aceptado una de las invitaciones pendientes de la página de inicio.
En este caso, la página muestra al usuario como contrincante y el estado de la partida
IN_PROGRESS
.
Cuando el usuario selecciona una opción en el tablero, se genera una solicitud POST
de formulario a la aplicación. Es decir, Flask llama a la función selectSquare
(en application.py
) con los datos del formulario HTML. Esta función, a su vez, llama a la función updateBoardAndTurn
(en gameController.py
) para actualizar el elemento de partida como se indica a continuación:
-
Agrega un nuevo atributo específico de la jugada.
-
Actualiza el valor del atributo
Turn
con el valor del usuario al que le toca jugar a continuación.
controller.updateBoardAndTurn(item, value, session["username"])
La función devuelve true si el elemento se actualizó correctamente; en caso contrario, devuelve false. Tenga en cuenta lo siguiente en relación con la función updateBoardAndTurn
:
-
La función llama a la función
update_item
del SDK para Python con el fin de realizar un conjunto de actualizaciones finitas de un elemento existente. La función se mapea a la operaciónUpdateItem
en DynamoDB. Para obtener más información, consulte UpdateItem.nota
La diferencia entre las operaciones
UpdateItem
yPutItem
es quePutItem
sustituye al elemento completo. Para obtener más información, consulte PutItem.
Para la llamada a update_item
, el código identifica lo siguiente:
-
Clave principal de la tabla
Games
(es decir,ItemId
).key = { "GameId" : { "S" : gameId } }
-
Nuevo atributo que se va a agregar, específico de la jugada del usuario actual, y su valor (por ejemplo,
TopLeft="X"
).attributeUpdates = { position : { "Action" : "PUT", "Value" : { "S" : representation } } }
-
Condiciones que deben cumplirse para que la actualización se lleve a cabo:
-
La partida debe estar en curso. Es decir, el valor del atributo
StatusDate
debe comenzar porIN_PROGRESS
. -
El turno actual debe ser válido para el usuario según lo especificado por el atributo
Turn
. -
La casilla elegida por el usuario debe estar disponible. Es decir, el atributo correspondiente a la casilla no debe existir.
expectations = {"StatusDate" : {"AttributeValueList": [{"S" : "IN_PROGRESS_"}], "ComparisonOperator": "BEGINS_WITH"}, "Turn" : {"Value" : {"S" : current_player}}, position : {"Exists" : False}}
-
Ahora, la función llama a update_item
para actualizar el elemento.
self.cm.db.update_item("Games", key=key, attribute_updates=attributeUpdates, expected=expectations)
Una vez que la función devuelva el resultado, la función selectSquare
llama a "redirect", tal y como se indica en el ejemplo siguiente.
redirect("/game="+gameId)
Esta llamada hace que el navegador se actualice. Durante esta actualización, la aplicación comprueba si la partida ha terminado con un ganador o en empate. En caso afirmativo, la aplicación actualiza el elemento de partida en consecuencia.