Paso 2: examinar el modelo de datos y los detalles de implementación - Amazon DynamoDB

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 como PENDING. En la siguiente captura de pantalla se muestra un ejemplo de elemento tal y como aparece en la consola de DynamoDB:

    Captura de pantalla de la consola de la tabla de atributos.

    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 o BottomRight. Por ejemplo, una jugada puede tener el atributo TopLeft con el valor O, un atributo TopRight con el valor O y un atributo BottomRight con el valor X. El valor del atributo puede ser O o X, según cuál sea el usuario que ha realizado la jugada. Por ejemplo, fíjese en el tablero siguiente.

    Captura de pantalla que muestra una partida de Tic-Tac-Toe (Tres en raya) finalizada que terminó en empate.
  • 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 y FINISHED) y la fecha (cuándo se realizó la última jugada), se combinan como un solo atributo; por ejemplo IN_PROGRESS_2014-04-30 10:20:32.

    A continuación, la aplicación utiliza el atributo StatusDate para crear índices secundarios especificando StatusDate como clave de ordenación del índice. El beneficio de utilizar el atributo de valores concatenados StatusDate 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:

    Captura de pantalla que muestra los índices secundarios globales HostStatusDate y OppStatusDate creados en la aplicación de ejemplo.
    • HostId-StatusDate-index. Este índice tiene HostId como clave de partición y StatusDate como clave de ordenación. Puede utilizar este índice para realizar una consulta sobre HostId, por ejemplo, para encontrar las partidas organizadas por un usuario concreto.

    • OpponentId-StatusDate-index. Este índice tiene OpponentId como clave de partición y StatusDate como clave de ordenación. Puede utilizar este índice para realizar una consulta sobre Opponent, 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 atributo IN_PROGRESS y que ha organizado un usuario determinado. En este caso, el operador BEGINS_WITH comprueba los valores de StatusDate que comienzan por IN_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 índice HostId-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 índice OpponentId-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.

Captura de pantalla que muestra la página de inicio de la aplicación con 3 listas: invitaciones pendientes, partidas en curso e historial reciente.
  • 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 índice beginswith="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 índice HostId-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ón BEGINS_WITH.

  • La función consulta la tabla Games mediante el índice OpponentId-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:

Captura de pantalla que muestra una partida de tres en raya 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ón UpdateItem en DynamoDB. Para obtener más información, consulte UpdateItem.

    nota

    La diferencia entre las operaciones UpdateItem y PutItem es que PutItem 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 por IN_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.