ステップ 2: データモデルと実装の詳細を調べます
2.1: 基本的なデータモデル
このサンプルアプリケーションでは、次の DynamoDB データモデルの概念を説明します。
-
テーブル – DynamoDB では、テーブルは項目 (つまりレコード) の集合であり、各項目には、属性と呼ばれる名前と値のペアが集約されています。
この Tic-Tac-Toe の例では、アプリケーションは、
Gamesテーブル内のすべてのゲームデータを保存します。アプリケーションはゲームごとにテーブルで 1 つの項目を作成し、すべてのゲームデータを属性として格納します。Tic-Tac-Toe ゲームでは最大 9 回の移動が可能です。DynamoDB テーブルは、プライマリキーのみが必須の属性である場合はスキーマを持たないので、アプリケーションは、ゲーム項目 ごとに異なる数の属性を格納できます。Gamesテーブルには、文字列型の 1 つの属性GameIdで構成されるシンプルなプライマリキーがあります。アプリケーションは各ゲームに一意の ID を割り当てます。DynamoDB でのプライマリキーの詳細については、「プライマリキー」を参照してください。ユーザーが他のユーザーを招待して Tic-Tac-Toe ゲームを開始すると、アプリケーションは、次のようなゲームメタデータを保存する属性で、
Gamesテーブルに新しい項目を作成します。-
HostIdゲームを開始したユーザーである 。 -
Opponentゲームに招待されたユーザーである 。 -
プレイする番のユーザー。最初にゲームを開始したユーザー。
-
ボードで O 記号を使用するユーザー。ゲームを開始するユーザーは O 記号を使用します。
さらに、アプリケーションは
StatusDate連結属性を作成し、ゲームの初期状態をPENDINGとしてマークします。次のスクリーンショットに、DynamoDB コンソールに表示される項目の例を示します。
アプリケーションは、ゲームの進行に合わせて、ゲームで動きがあるたびに 1 つの属性をテーブルに追加します。属性名はボードの位置(
TopLeft、BottomRightなど)です。たとえば、動きには値TopLeftのO属性、値TopRightのO属性、および値BottomRightのX属性があるなどです。属性値は、動いたユーザーに応じて、OまたはXになります。たとえば、次のボードを考えてみます。
-
-
連結属性値 –
StatusDate属性が、連結値属性を示します。この手法では、ゲームのステータス(PENDING、IN_PROGRESS、FINISHED)および日付(最後の動き)を格納する個別の属性を作成する代わりに、IN_PROGRESS_2014-04-30 10:20:32などの単一の属性としてそれらを組み合わせます。次に、アプリケーションは、インデックスのソートキーとして
StatusDateを指定して、セカンダリインデックスの作成でStatusDate属性を使用します。StatusDate連結値属性を使用する利点について、さらに次のインデックスの説明で示します。 -
グローバルセカンダリインデックス – テーブルのプライマリキー、
GameIdを使用すると、テーブルを効率的に照会してゲーム項目を検索できます。プライマリキー属性以外の属性でテーブルをクエリするため、DynamoDB ではセカンダリインデックスの作成をサポートしています。このサンプルアプリケーションでは、次の 2 つのセカンダリインデックスを構築します。
-
HostId-StatusDate-index。このインデックスはパーティションキーとして
HostIdを、ソートキーとしてStatusDateを持ちます。このインデックスを使用して、たとえば特定のユーザーによってホストされたゲームを検索するなど、HostIdでクエリを実行できます。 -
OpponentId-StatusDate-index。このインデックスはパーティションキーとして
OpponentIdを、ソートキーとしてStatusDateを持ちます。たとえば、特定のユーザーが対戦相手であるゲームを検索するなど、このインデックスを使用してOpponentでクエリを実行できます。
これらのインデックスは、グローバルセカンダリインデックスと呼ばれます。これは、インデックスのパーティションキーが、テーブルのプライマリキーで使用されるパーティションキー (
GameId) とは同じでないためです。両方のインデックスがソートキーとして
StatusDateを指定することに注意してください。これを行うと、次のことが可能になります。-
BEGINS_WITH比較演算子を使用してクエリを実行できます。たとえば、特定のユーザーによってホストされたIN_PROGRESS属性を持つすべてのゲームを検索できます。この場合、BEGINS_WITH演算子は、StatusDateで始まるIN_PROGRESS値を確認します。 -
DynamoDB は、項目をソートキー値によって整列させ、インデックスに保存します。したがって、すべてのステータスのプレフィックスは同じ(たとえば、
IN_PROGRESS)になり、日付部分に使用される ISO 形式には、最も古いものから最も新しいものの順にソートされた項目が含まれます。この手法により、たとえば次のように特定のクエリを効率的に実行できるようになります。-
ログインしているユーザーによってホストされている最新の
IN_PROGRESSのゲームを最大 10 個取得します。このクエリでは、HostId-StatusDate-indexインデックスを指定します。 -
ログインしているユーザーが対戦相手である最新の
IN_PROGRESSのゲームを最大 10 個取得します。このクエリでは、OpponentId-StatusDate-indexインデックスを指定します。
-
-
セカンダリインデックスの詳細については、「DynamoDB でのセカンダリインデックスを使用したデータアクセス性の向上」を参照してください。
2.2: 実行中のアプリケーション (コードのウォークスルー)
このアプリケーションには 2 つのメインページがあります。
-
ホームページ – このページには、簡単なログイン、新しい tic-tac-toe ゲームを作成する [CREATE] (作成) ボタン、進行中のゲームのリスト、ゲームの履歴、およびアクティブな保留中のゲームの招待が表示されます。
ホームページは自動的に更新されません。リストを更新するには、ユーザーがページを更新する必要があります。
-
ゲームページ – このページには、ユーザーがプレイするための、tic-tac-toe グリッドが表示されます。
アプリケーションは、毎秒ゲームページを自動的に更新します。ブラウザの JavaScript が毎秒 Python ウェブサーバーを呼び出して、テーブルのゲーム項目が変更されたかどうかを Games テーブルに照会します。変更された場合、JavaScript はページ更新をトリガーし、更新されたボードがユーザーに表示されるようにします。
アプリケーションの動作について詳細に説明します。
ホームページ
ユーザーがログインすると、アプリケーションには、以下の 3 つの情報のリストが表示されます。
-
招待 – このリストには、ログインしているユーザーが受け入れを保留中の、他のユーザーからの最新の招待が最大 10 個表示されます。前のスクリーンショットで、user1 は user5 および user2 からの招待を保留中です。
-
進行中のゲーム – このリストには、進行中の最新のゲームが最大 10 個表示されます。これらは、ユーザーがアクティブに実行中のゲームで、そのステータスは
IN_PROGRESSです。スクリーンショットでは、user1 は user3 および user4 と Tic-Tac-Toe ゲームをアクティブにプレイ中です。 -
最近の履歴 – このリストには、ユーザーが終了した最近のゲームが最大 10 個表示されます。そのステータスは
FINISHEDです。スクリーンショットに示したゲームで、user1 は以前に user2 とプレイしています。完了した各ゲームについて、ゲームの結果がリストに表示されます。
コードで、index 関数は(application.py で)次の 3 つの呼び出しを行い、ゲームのステータス情報を取得します。
inviteGames = controller.getGameInvites(session["username"]) inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")
これらの呼び出しはそれぞれ、Game オブジェクトでラップされた、DynamoDB からの項目のリストを返します。ビューでこれらのオブジェクトからデータを抽出することは簡単です。インデックス関数は、HTML を表示するため、これらのオブジェクトリストをビューに渡します。
return render_template("index.html", user=session["username"], invites=inviteGames, inprogress=inProgressGames, finished=finishedGames)
Tic-Tac-Toe アプリケーションは、主に DynamoDB から取得したゲームデータを格納するため、Game クラスを定義します。これらの関数は、Amazon DynamoDB の項目に関連するコードからアプリケーションの他の部分を分離できるようにするために、Game オブジェクトのリストを返します。これらの関数により、このようにしてデータストア層の詳細からアプリケーションコードを切り離すことができます。
ここで説明するアーキテクチャーパターンは、model-view-controller(MVC)UI パターンとも呼ばれます。この場合、Game オブジェクトのインスタンス(データを表す)はモデルで、HTML ページはビューです。コントローラーは 2 つのファイルに分割されます。application.py ファイルには Flask フレームワーク用のコントローラーロジックがあり、ビジネスロジックは gameController.py ファイルに分離されます。つまり、このアプリケーションでは、DynamoDB SDK に関連のあるすべてが、dynamodb フォルダ内にある独自の個別ファイルに格納されます。
3 つの関数と、それらが該当データを取得するためにグローバルセカンダリインデックスを使用して Games テーブルを照会する方法について説明します。
getGameInvites を使用して保留中のゲームの招待リストを取得します
getGameInvites 関数は保留中の最新の 10 個の招待リストを取得します。これらのゲームはユーザーによって作成されましたが、対戦相手はゲームの招待を受け入れていません。これらのゲームの場合、対戦相手が招待を受け入れるまでステータスは PENDING のままになります。対戦相手が招待を辞退した場合、アプリケーションは、対応する項目をテーブルから削除します。
関数は、以下のようなクエリを指定します。
-
関数は、以下のキー値および比較演算子とともに使用する
OpponentId-StatusDate-indexインデックスを指定します。-
パーティションキーは
OpponentIdで、インデックスキーを受け取ります。user ID -
ソートキーは
StatusDateで、比較演算子およびインデックスキー値beginswith="PENDING_"を受け取ります。
OpponentId-StatusDate-indexインデックスを使用して、ログインしているユーザーが招待されているゲーム、つまりログインしているユーザーが対戦相手であるゲームを取得します。 -
-
クエリは結果を 10 項目に制限します。
gameInvitesIndex = self.cm.getGamesTable().query( Opponent__eq=user, StatusDate__beginswith="PENDING_", index="OpponentId-StatusDate-index", limit=10)
このインデックスでは、OpponentId (パーティションキー) ごとに、DynamoDB が項目を StatusDate (ソートキー) により整列させて保持しています。そのため、クエリが返すゲームは最新の 10 個のゲームになります。
getGamesWithStatus を使用して特定のステータスのゲームリストの取得します
対戦相手がゲームの招待を受け入れると、ゲームのステータスは IN_PROGRESS に変わります。ゲームが完了すると、ステータスは FINISHED に変わります。
進行中または終了済みのゲームを検索するクエリは、ステータス値が異なる場合を除いて同じです。したがって、アプリケーションは getGamesWithStatus 関数を定義し、この関数はステータス値をパラメータとして受け取ります。
inProgressGames = controller.getGamesWithStatus(session["username"], "IN_PROGRESS") finishedGames = controller.getGamesWithStatus(session["username"], "FINISHED")
次のセクションでは進行中のゲームについて説明しますが、終了済みのゲームにも同じ説明が当てはまります。
特定のユーザーの進行中のゲームのリストには、次の両方が含まれます。
-
ユーザーによってホストされた進行中のゲーム
-
ユーザーが対戦相手となっている進行中のゲーム
getGamesWithStatus 関数は、毎回適切なセカンダリインデックスを使用して次の 2 つのクエリを実行します。
-
この関数は
Gamesインデックスを使用してHostId-StatusDate-indexテーブルをクエリします。このインデックスのためにクエリは、プライマリキー値つまりパーティションキー (HostId) およびソートキー (StatusDate) の値と、比較演算子を指定します。hostGamesInProgress = self.cm.getGamesTable ().query(HostId__eq=user, StatusDate__beginswith=status, index="HostId-StatusDate-index", limit=10)比較演算子の Python の構文に注意してください。
-
HostId__eq=userは等価比較演算子を指定します。 -
StatusDate__beginswith=statusはBEGINS_WITH比較演算子を指定します。
-
-
この関数は
Gamesインデックスを使用してOpponentId-StatusDate-indexテーブルをクエリします。oppGamesInProgress = self.cm.getGamesTable().query(Opponent__eq=user, StatusDate__beginswith=status, index="OpponentId-StatusDate-index", limit=10) -
次に、この関数は 2 つのリストを組み合わせ、ソートし、最初の 0~10 項目について
Gameオブジェクトのリストを作成して、呼び出し元関数(インデックス)にリストを返します。games = self.mergeQueries(hostGamesInProgress, oppGamesInProgress) return games
ゲームページ
ゲームページは、ユーザーが Tic-Tac-Toe ゲームをプレイする場所です。このページには、ゲームの関連情報とともに、ゲームグリッドが表示されます。次のスクリーンショットは、進行中のサンプルゲームを示しています。
アプリケーションは次の状況でゲームページを表示します。
-
ユーザーは他のユーザーを招待してゲームを作成します。
この場合、ページはホストやゲームのステータスを
PENDINGとしてユーザーに表示し、対戦相手が受け入れるのを待ちます。 -
ユーザーは、ホームページで保留中の招待の 1 つを受け入れます。
この場合、ページではユーザーは対戦相手として、ゲームのステータスは
IN_PROGRESSとして表示されます。
ボード上でのユーザーによる選択操作により、アプリケーションへのフォーム POST リクエストが生成されます。つまり、Flask は HTML フォームデータとともに selectSquare 関数を(application.py で)呼び出します。次に、この関数は updateBoardAndTurn 関数を(gameController.py で)呼び出して、次のようにゲーム項目を更新します。
-
これにより、動きに固有の新しい属性が追加されます。
-
Turn属性が、次の番のユーザーに更新されます。
controller.updateBoardAndTurn(item, value, session["username"])
項目の更新が成功した場合、関数は true を返します。それ以外の場合は、false を返します。updateBoardAndTurn 関数について、以下の点に注意してください。
-
この関数は、既存の項目に対する一定範囲の更新を実行するため、SDK for Python の
update_item関数を呼び出します。この関数は、DynamoDB のUpdateItemオペレーションにマッピングされます。詳細については、「UpdateItem」を参照してください。注記
UpdateItemオペレーションとPutItemオペレーションの違いは、PutItemが項目全体を置き換えることです。詳細については、「PutItem」を参照してください。
update_item 呼び出しでは、コードは以下を識別します。
-
Gamesテーブルのプライマリキー (ItemId)。key = { "GameId" : { "S" : gameId } } -
現在のユーザーの動きに固有の、追加する新しい属性とその値(例:
TopLeft="X")。attributeUpdates = { position : { "Action" : "PUT", "Value" : { "S" : representation } } } -
更新が実行されるために満たされる必要がある条件
-
ゲームは進行中である必要があります。つまり、
StatusDate属性値はIN_PROGRESSで始まる必要があります。 -
現在の番は、
Turn属性で指定されている有効なユーザーの番である必要があります。 -
ユーザーが選択した四角形は使用可能である必要があります。つまり、四角形に対応する属性は存在していてはなりません。
expectations = {"StatusDate" : {"AttributeValueList": [{"S" : "IN_PROGRESS_"}], "ComparisonOperator": "BEGINS_WITH"}, "Turn" : {"Value" : {"S" : current_player}}, position : {"Exists" : False}} -
ここで、関数は update_item を呼び出して項目を更新します。
self.cm.db.update_item("Games", key=key, attribute_updates=attributeUpdates, expected=expectations)
関数に戻ると、selectSquare 関数呼び出しは次の例に示すようにリダイレクトされます。
redirect("/game="+gameId)
この呼び出しにより、ブラウザが更新されます。この更新の一環として、アプリケーションはゲームが優勝または引き分けで終了したかどうかを確認します。終了した場合、アプリケーションはそれに応じてゲーム項目を更新します。