DynamoDB と NoSQL を初めて使用するユーザーが調整を要する最たることに、データをモデル化してデータセット全体をフィルタリングする方法があります。たとえば、ゲームでは、空きスポットのあるゲームセッションを見つけて、ユーザーが参加できるゲームセッションをユーザーに示す必要があります。
リレーショナルデータベースでは、データをクエリする SQL を作成します。
SELECT * FROM games WHERE status = “OPEN”
DynamoDB は、Query または Scan 操作で結果をフィルタリングできますが、DynamoDB はリレーショナルデータベースのようには機能しません。DynamoDB フィルターは、Query または Scan 操作に一致する初期アイテムを取得した後に適用されます。このフィルターは、DynamoDB サービスから送信されるペイロードのサイズを縮小しますが、最初に取得するアイテムの数は DynamoDB のサイズ制限の影響を受けます。
幸い、DynamoDB のデータセットに対してフィルタリングされたクエリを許可する方法はいくつかあります。DynamoDB テーブルに対して効率的にフィルタリングするには、最初からテーブルのデータモデルにフィルターを計画する必要があります。このラボの 2 番目のモジュールで学んだ教訓を思い出してください。アクセスパターンを検討してから、テーブルを設計します。
次の手順では、グローバルセカンダリインデックスを使用して、オープンゲームを見つけます。具体的には、スパースインデックス手法を用いてこのアクセスパターンを処理します。
モジュールの所要時間: 40 分
-
ステップ 1: スパースセカンダリインデックスをモデル化する
セカンダリインデックスは、DynamoDB の重要なデータモデリングツールです。これにより、データを変更して、代替のクエリパターンを使用できます。セカンダリインデックスを作成するには、以前にテーブルを作成したときと同じように、インデックスのプライマリキーを指定します。グローバルセカンダリインデックスのプライマリキーは、アイテムごとに一意である必要はありません。次に、DynamoDB は指定された属性に基づいてアイテムをインデックスにコピーし、テーブルと同じようにクエリを実行できます。
スパースセカンダリインデックスの使用は、DynamoDB の高度な戦略です。セカンダリインデックスを使用すると、DynamoDB は、セカンダリインデックスにプライマリキーの要素がある場合にのみ、元のテーブルからアイテムをコピーします。プライマリキー要素を持たないアイテムはコピーされないため、このセカンダリインデックスは「スパース」と呼ばれます。
これがどのように機能するかを見てみましょう。オープンゲームを見つけるために次の 2 つのアクセスパターンがありました。
- オープンゲームを見つける (読み取り)
- マップでオープンゲームを見つける (読み取り)
HASH キーがゲームの map 属性で、RANGE キーがゲームの open_timestamp 属性 である複合プライマリキーを使用してセカンダリインデックスを作成でき、ゲームがオープンになっていた時間を示します。
私たちにとって重要な部分は、ゲームがいっぱいになると、open_timestamp 属性が削除されることです。属性が削除されると、RANGE キー属性の値がないため、いっぱいになったゲームはセカンダリインデックスから削除されます。これにより、インデックスがスパースに保たれます。インデックスには、open_timestamp 属性を持つオープンゲームのみが含まれます。
次の手順では、セカンダリインデックスを作成します。
-
ステップ 2: スパースセカンダリインデックスを作成する
このステップでは、オープンゲーム (まだいっぱいではないゲーム) のスパースセカンダリインデックスを作成します。
セカンダリインデックスを作成することは、テーブルを作成することに似ています。ダウンロードしたコードには、scripts/ ディレクトリに add_secondary_index.py という名前のスクリプトファイルがあります。そのファイルの内容は次のとおりです。
import boto3 dynamodb = boto3.client('dynamodb') try: dynamodb.update_table( TableName='battle-royale', AttributeDefinitions=[ { "AttributeName": "map", "AttributeType": "S" }, { "AttributeName": "open_timestamp", "AttributeType": "S" } ], GlobalSecondaryIndexUpdates=[ { "Create": { "IndexName": "OpenGamesIndex", "KeySchema": [ { "AttributeName": "map", "KeyType": "HASH" }, { "AttributeName": "open_timestamp", "KeyType": "RANGE" } ], "Projection": { "ProjectionType": "ALL" }, "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 } } } ], ) print("Table updated successfully.") except Exception as e: print("Could not update table. Error:") print(e)
テーブルまたはセカンダリインデックスのプライマリキーで属性を使用する場合は、AttributeDefinitions で定義する必要があります。次に、GlobalSecondaryIndexUpdates プロパティに新しいセカンダリインデックスを Create します。このセカンダリインデックスには、インデックス名、プライマリキーのスキーマ、プロビジョニングされたスループット、および投影する属性を指定します。
セカンダリインデックスをスパースインデックスとして使用することを指定する必要はないことに注意してください。それは純粋にユーザーが入れたデータの関数です。セカンダリインデックスの属性を持たないアイテムをテーブルに書き込む場合、それらのアイテムはセカンダリインデックスに含まれません。
次のコマンドを実行して、セカンダリインデックスを作成します。
python scripts/add_secondary_index.py
コンソールに「Table updated successfully」というメッセージが表示されるはずです。
次のステップでは、スパースインデックスを使用して、マップでオープンゲームを見つけます。
-
ステップ 3: スパースセカンダリインデックスをクエリする
セカンダリインデックスを設定したので、これを使用してアクセスパターンのいくつかを満たすことにしましょう。
セカンダリインデックスを使用するには、Query と Scan の 2 つの API 呼び出しを使用できます。Query では、HASH キーを指定する必要があり、ターゲットの結果を返します。Scan では、HASH キーを指定することなく、操作がテーブル全体で実行されます。Scan は、データベース内のすべてのアイテムにアクセスするため、特定の状況を除き、DynamoDB では推奨されません。テーブルに大量のデータがある場合、スキャンには非常に長い時間がかかる可能性があります。次のステップでは、Scan をスパースインデックスで使用しる場合に強力なツールになる理由を示します。
前の手順で作成したセカンダリインデックスに対して Query API を使用して、すべてのオープンゲームをマップ名で検索できます。セカンダリインデックスはマップ名でパーティション化されているため、ターゲットを絞ったクエリを実行して、オープンゲームを見つけることができます。
ダウンロードしたコードでは、find_open_games_by_map.py ファイルは application/ ディレクトリにあります。このスクリプトの内容は次のとおりです。
import boto3 from entities import Game dynamodb = boto3.client('dynamodb') def find_open_games_by_map(map_name): resp = dynamodb.query( TableName='battle-royale', IndexName="OpenGamesIndex", KeyConditionExpression="#map = :map", ExpressionAttributeNames={ "#map": "map" }, ExpressionAttributeValues={ ":map": { "S": map_name }, }, ScanIndexForward=True ) games = [Game(item) for item in resp['Items']] return games games = find_open_games_by_map("Green Grasslands") for game in games: print(game)
上記のスクリプトでは、find_open_games_by_map 関数は、アプリケーションにある関数に似ています。この関数はマップ名を受け入れ、OpenGamesIndex に対してクエリを実行して、マップのすべてのオープンゲームを見つけます。次に、返されたエンティティを、アプリケーションで使用できる Game オブジェクトにアセンブルします。
ターミナルで次のコマンドを実行して、このスクリプトを実行します。
python application/find_open_games_by_map.py
ターミナルは次の出力を表示し、Green Grasslands マップに 4 つのオープンゲームがあることがわかります。
Open games for Green Grasslands: Game<14c7f97e-8354-4ddf-985f-074970818215 -- Green Grasslands> Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands> Game<683680f0-02b0-4e5e-a36a-be4e00fc93f3 -- Green Grasslands> Game<0ab37cf1-fc60-4d93-b72b-89335f759581 -- Green Grasslands> sudo cp -r wordpress/* /var/www/html/
次のステップでは、Scan API を使用して、スパースセカンダリインデックスをスキャンします。
-
ステップ 4: スパースセカンダリインデックスをスキャンする
前のステップで、特定のマップのゲームを見つける方法を見てきました。一部のプレイヤーは特定のマップをプレイすることを好む場合があるため、これは便利です。どのマップでも喜んでゲームをプレイするプレイヤーもいることでしょう。このセクションでは、マップのタイプに関係なく、アプリケーションでオープンゲームを見つける方法を示します。これを行うには、Scan API を使用します。
一般に、DynamoDB は必要なエンティティを正確に取得する外科的クエリ用に構築されているため、DynamoDB Scan 操作を行うようにテーブルを設計する必要はありません。Scan 操作は、テーブル全体のエンティティのランダムコレクションを取得するため、必要なエンティティを見つけるには、データベースを何度も行ったり来たりすることになる可能性があります。
ただし、Scan が役立つ場合もあります。ここには、スパースセカンダリインデックスがあります。つまり、インデックスにそれほど多くのエンティティが含まれてはいけません。さらに、インデックスにはオープンゲームのみが含まれており、まさにそれが求めているものです。
このユースケースでは、Scan がうまく機能します。仕組みを見てみましょう。ダウンロードしたコードでは、find_open_games.py ファイルは application/ ディレクトリにあります。ファイルの内容は次のとおりです。
import boto3 from entities import Game dynamodb = boto3.client('dynamodb') def find_open_games(): resp = dynamodb.scan( TableName='battle-royale', IndexName="OpenGamesIndex", ) games = [Game(item) for item in resp['Items']] return games games = find_open_games() print("Open games:") for game in games: print(game)
このコードは、前のステップのコードに似ています。ただし、DynamoDB クライアントで query() メソッドを使用するのではなく、scan() メソッドを使用します。scan() を使用しているため、query() で行ったようにキー条件について何も指定する必要はありません。DynamoDB に、順序を特定することなく複数のアイテムを返すようにしています。
ターミナルで次のコマンドを使用してスクリプトを実行します。
python application/find_open_games.py
ターミナルは、さまざまなマップで開かれている 9 つのゲームのリストを印刷する必要があります。
Open games: Game<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- Urban Underground> Game<d06af94a-2363-441d-a69b-49e3f85e748a -- Dirty Desert> Game<873aaf13-0847-4661-ba26-21e0c66ebe64 -- Dirty Desert> Game<fe89e561-8a93-4e08-84d8-efa88bef383d -- Dirty Desert> Game<248dd9ef-6b17-42f0-9567-2cbd3dd63174 -- Juicy Jungle> Game<14c7f97e-8354-4ddf-985f-074970818215 -- Green Grasslands> Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands> Game<683680f0-02b0-4e5e-a36a-be4e00fc93f3 -- Green Grasslands> Game<0ab37cf1-fc60-4d93-b72b-89335f759581 -- Green Grasslands>
このステップでは、特定の状況では Scan 操作を使用することが正しい選択であることを見ました。Scan を使用して、疎なセカンダリインデックスからさまざまなエンティティを取得し、オープンゲームをプレイヤーに表示しました。
次の手順では、2 つのアクセスパターンを満たします。
- ユーザーがゲームに参加 (書き込み)
- ゲームを開始 (書き込み)
次の手順で「ユーザーがゲームに参加」アクセスパターンを満たすために、DynamoDB トランザクションを使用します。トランザクションはリレーショナルシステムで一般的に使われ、複数のデータ要素に一度に影響を与える操作を行います。たとえば、銀行を経営しているとします。顧客の Alejandra は、100 USD を別の顧客の Ana に振り替えます。このトランザクションを記録するときは、トランザクションを使用して、一方だけではなく両方の顧客の残高に変更が適用されるようにします。
DynamoDB トランザクションでは、単一の操作の一部として複数のアイテムを変更するアプリケーションを簡単に構築できます。トランザクションにより、1 つのトランザクションリクエストの一部として最大 10 個のアイテムを操作できます。
TransactWriteItem API 呼び出しでは、次の操作を行えます。
- Put: アイテムを挿入または上書きします。
- Update: 既存のアイテムを更新します。
- Delete: アイテムを削除します。
- ConditionCheck: アイテムを変更せずに、既存のアイテムに条件をアサートします。
次のステップでは、ゲームがいっぱいにならないようにしながら、新しいユーザーをゲームに追加するときに DynamoDB トランザクションを使用します。
-
ステップ 5: ユーザーをゲームに追加する
このモジュールで対処する最初のアクセスパターンは、新しいユーザーをゲームに追加することです。
新しいユーザーをゲームに追加するとき、次のことが必要です。
- ゲームの参加しているプレイヤーが 50 人に達していないことを確認します (各ゲームには最大 50 人のプレイヤーを含めることができます)。
- ユーザーがまだゲームに参加していないことを確認します。
- 新しい UserGameMapping エンティティを作成して、ユーザーをゲームに追加します。
- Game エンティティの people 属性をインクリメントして、ゲームに参加しているプレイヤーの数を追跡します。
これらのすべてを達成するには、既存の Game エンティティと新しい UserGameMapping エンティティに対する書き込みアクションと、各エンティティの条件ロジックが必要であることに注意してください。これは、DynamoDB トランザクションに最適な種類の操作です。ここでは同じリクエストで複数のエンティティを操作し、リクエスト全体を一緒に成功または失敗させる必要があるためです。
ダウンロードしたコードでは、join_game.py スクリプトは application/ ディレクトリにあります。このスクリプトの関数は、DynamoDB トランザクションを使用してユーザーをゲームに追加します。
スクリプトの内容は次のとおりです。
import boto3 from entities import Game, UserGameMapping dynamodb = boto3.client('dynamodb') GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646" USERNAME = 'vlopez' def join_game_for_user(game_id, username): try: resp = dynamodb.transact_write_items( TransactItems=[ { "Put": { "TableName": "battle-royale", "Item": { "PK": {"S": "GAME#{}".format(game_id) }, "SK": {"S": "USER#{}".format(username) }, "game_id": {"S": game_id }, "username": {"S": username } }, "ConditionExpression": "attribute_not_exists(SK)", "ReturnValuesOnConditionCheckFailure": "ALL_OLD" }, }, { "Update": { "TableName": "battle-royale", "Key": { "PK": { "S": "GAME#{}".format(game_id) }, "SK": { "S": "#METADATA#{}".format(game_id) }, }, "UpdateExpression": "SET people = people + :p", "ConditionExpression": "people <= :limit", "ExpressionAttributeValues": { ":p": { "N": "1" }, ":limit": { "N": "50" } }, "ReturnValuesOnConditionCheckFailure": "ALL_OLD" } } ] ) print("Added {} to game {}".format(username, game_id)) return True except Exception as e: print("Could not add user to game") join_game_for_user(GAME_ID, USERNAME)
このスクリプトの join_game_for_user 関数では、transact_write_items() メソッドが書き込みトランザクションを実行します。このトランザクションには 2 つの操作があります。
トランザクションの最初の操作では、Put 操作を使用して、新しい UserGameMapping エンティティを挿入します。その操作の一部として、SK 属性がこのエンティティに存在しないという条件を指定します。これにより、この PK と SK を持つエンティティがまだ存在しないことが保証されます。そのようなエンティティがすでに存在する場合、このユーザーはすでにゲームに参加していることを意味します。
2 番目の操作は、Game エンティティの Update 操作で、people 属性を 1 つ増やします。この操作の一部として、people の現在の値が 50 以下であるという条件を追加でチェックします。50 人がゲームに参加するとすぐに、ゲームはいっぱいになり、開始する準備が整います。
ターミナルで次のコマンドを使用してこのスクリプトを実行します。
python application/join_game.py
ターミナルで出力されたということは、ユーザーがゲームに追加されたことを示しています。
Added vlopez to game c6f38a6a-d1c5-4bdf-8468-24692ccc4646
スクリプトを再度実行しようとすると、関数が失敗することに注意してください。ユーザー vlopez はすでにゲームに追加されているため、ユーザーを再度追加しようとしても、指定した条件を満たしません。
DynamoDB トランザクションの追加により、このような複雑な操作に関するワークフローが大幅に簡素化されます。トランザクションがなければ、競合が発生した場合、複雑な条件と手動のロールバックを伴う複数の API 呼び出しが必要になります。これで、50 行未満のコードでこのような複雑な操作を実装できるようになりました。
次のステップでは、「ゲームを開始 (書き込み) 」アクセスパターンを処理します。
-
ステップ 6: ゲームを開始する
ゲームに 50 人のユーザーがいるとすぐに、ゲームの作成者はゲームを開始してゲームプレイを始められます。このステップでは、このアクセスパターンの処理方法を示します。
アプリケーションバックエンドがゲームを開始するリクエストを受信すると、次の 3 つのことを確認します。
- ゲームには 50 人がサインアップしている。
- リクエスト元のユーザーはゲームの作成者である。
- ゲームはまだ開始されていない。
ゲームを更新するリクエストの条件式でこれらの各チェックを処理できます。これらのすべてのチェックに問題がなかった場合、次の方法でエンティティを更新する必要があります。
- open_timestamp 属性を削除して、前のモジュールのスパースセカンダリインデックスにオープンゲームとして表示されないようにします。
- ゲームの開始時刻を示す start_time 属性を追加します。
ダウンロードしたコードでは、start_game.py スクリプトは application/ ディレクトリにあります。ファイルの内容は次のとおりです。
import datetime import boto3 from entities import Game dynamodb = boto3.client('dynamodb') GAME_ID = "c6f38a6a-d1c5-4bdf-8468-24692ccc4646" CREATOR = "gstanley" def start_game(game_id, requesting_user, start_time): try: resp = dynamodb.update_item( TableName='battle-royale', Key={ "PK": { "S": "GAME#{}".format(game_id) }, "SK": { "S": "#METADATA#{}".format(game_id) } }, UpdateExpression="REMOVE open_timestamp SET start_time = :time", ConditionExpression="people = :limit AND creator = :requesting_user AND attribute_not_exists(start_time)", ExpressionAttributeValues={ ":time": { "S": start_time.isoformat() }, ":limit": { "N": "50" }, ":requesting_user": { "S": requesting_user } }, ReturnValues="ALL_NEW" ) return Game(resp['Attributes']) except Exception as e: print('Could not start game') return False game = start_game(GAME_ID, CREATOR, datetime.datetime(2019, 4, 16, 10, 15, 35)) if game: print("Started game: {}".format(game))
このスクリプトでは、start_game 関数はアプリケーションにある関数に似ています。game_id、requesting_user、および start_time で、Game エンティティを更新してゲームを開始するリクエストを実行します。
update_item() 呼び出しの ConditionExpression パラメータは、このステップで前述した 3 つのチェックをそれぞれ指定します。その 3 つとは、ゲームには 50 人が必要で、ゲームの開始をリクエストするユーザーはゲームの作成者で、ゲームは start_time 属性を持つことはできない (この属性があると、ゲームがすでに開始されていることを示します) というものでした。
UpdateExpression パラメータで、エンティティに加えたい変更を確認できます。まず、エンティティから open_timestamp 属性を削除し、次に start_time 属性をゲームの開始時間に設定します。
次のコマンドを使用して、ターミナルでこのスクリプトを実行します。
python application/start_game.py
ゲームが正常に開始されたことを示す出力がターミナルに表示されるはずです。
Started game: Game<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- Urban Underground>
ターミナルでスクリプトをもう一度実行してみてください。今回は、ゲームを開始できなかったことを示すエラーメッセージが表示されるはずです。これは、すでにゲームを開始しているため、start_time 属性が存在するためです。その結果、リクエストはエンティティの条件付きチェックに失敗しました。
ゲームエンティティと関連するユーザーエンティティ間に多対多のリレーションシップがあり、そのリレーションシップは UserGameMapping エンティティによって表されることを覚えていることでしょう。
多くの場合、リレーションシップの両側をクエリする必要があります。プライマリキーの設定により、Game 内のすべての User エンティティを見つけることができます。転置インデックスを使用して、User のすべての Game エンティティのクエリを有効にできます。
DynamoDB では、転置インデックスは、プライマリキーの逆であるセカンダリインデックスです。RANGE キーは HASH キーになり、逆も同様です。このパターンはテーブルを反転させ、多対多のリレーションシップの反対側でクエリを実行できるようにします。
次の手順では、テーブルに転置インデックスを追加し、それを使用して特定の User のすべての Game エンティティを取得する方法を示します。
-
ステップ 7: 転置インデックスを追加する
このステップでは、テーブルに転置インデックスを追加します。転置インデックスは、他のセカンダリインデックスと同様に作成されます。
ダウンロードしたコードでは、add_inverted_index.py スクリプトは scripts/ ディレクトリにあります。この Python スクリプトは、テーブルに転置インデックスを追加します。
そのファイルの内容は次のとおりです。
import boto3 dynamodb = boto3.client('dynamodb') try: dynamodb.update_table( TableName='battle-royale', AttributeDefinitions=[ { "AttributeName": "PK", "AttributeType": "S" }, { "AttributeName": "SK", "AttributeType": "S" } ], GlobalSecondaryIndexUpdates=[ { "Create": { "IndexName": "InvertedIndex", "KeySchema": [ { "AttributeName": "SK", "KeyType": "HASH" }, { "AttributeName": "PK", "KeyType": "RANGE" } ], "Projection": { "ProjectionType": "ALL" }, "ProvisionedThroughput": { "ReadCapacityUnits": 1, "WriteCapacityUnits": 1 } } } ], ) print("Table updated successfully.") except Exception as e: print("Could not update table. Error:") print(e)
このスクリプトでは、DynamoDB クライアントで update_table() メソッドを呼び出します。このメソッドでは、インデックスのキースキーマ、プロビジョニングされたスループット、インデックスに投影する属性など、作成するセカンダリインデックスに関する詳細を渡します。
ターミナルで次のコマンドを入力して、スクリプトを実行します。
python scripts/add_inverted_index.py
ターミナルには、インデックスが正常に作成されたという出力が表示されます。
Table updated successfully.
次のステップでは、転置インデックスを使用して、特定の User のすべての Game エンティティを取得します。
-
ステップ 8: ユーザーのゲームを取得する
転置インデックスを作成したので、これを使用して、ある User がプレイした Game エンティティを取得しましょう。これを処理するには、Game エンティティを表示する User で転置インデックスをクエリする必要があります。
ダウンロードしたコードでは、find_games_for_user.py スクリプトは application/ ディレクトリにあります。ファイルの内容は次のとおりです。
import boto3 from entities import UserGameMapping dynamodb = boto3.client('dynamodb') USERNAME = "carrpatrick" def find_games_for_user(username): try: resp = dynamodb.query( TableName='battle-royale', IndexName='InvertedIndex', KeyConditionExpression="SK = :sk", ExpressionAttributeValues={ ":sk": { "S": "USER#{}".format(username) } }, ScanIndexForward=True ) except Exception as e: print('Index is still backfilling. Please try again in a moment.') return None return [UserGameMapping(item) for item in resp['Items']] games = find_games_for_user(USERNAME) if games: print("Games played by {}:".format(USERNAME)) for game in games: print(game)
このスクリプトには、find_games_for_user() という関数があります。これは、ゲームに存在する関数に似ています。この関数はユーザー名を受け取り、そのユーザーがプレイしたすべてのゲームを返します。
ターミナルで次のコマンドを使用してスクリプトを実行します。
python application/find_games_for_user.py
スクリプトは、ユーザー carrpatrick がプレイしたすべてのゲームをプリントする必要があります。
Games played by carrpatrick: UserGameMapping<25cec5bf-e498-483e-9a00-a5f93b9ea7c7 -- carrpatrick -- SILVER> UserGameMapping<c6f38a6a-d1c5-4bdf-8468-24692ccc4646 -- carrpatrick> UserGameMapping<c9c3917e-30f3-4ba4-82c4-2e9a0e4d1cfd -- carrpatrick>
このモジュールでは、テーブルにセカンダリインデックスを追加しました。これにより、次の 2 つの追加アクセスパターンが満たされました。
- マップでオープンゲームを見つける (読み取り)
- オープンゲームを見つける (読み取り)
これを達成するために、追加のプレイヤーにまだオープンであるゲームのみを含めたスパースインデックスを使用しました。次に、インデックスに対して Query API と Scan API の両方を使用して、オープンゲームを見つけました。
また、アプリケーションでの 2 つの高度な書き込み操作を満たす方法についても説明しました。最初に、ユーザーがゲームに参加したときに DynamoDB トランザクションを使用しました。トランザクションでは、1 つのリクエストで複数のエンティティにわたる複雑な条件付き書き込みを処理しました。
次に、ゲームの作成者が準備ができたらゲームを開始する機能を実装しました。このアクセスパターンでは、3 つの属性の値を確認し、2 つの属性を更新する必要がある更新操作がありました。この複雑なロジックは、条件式と更新式の力により、1 つのリクエストで表現できます。
3 番目に、ある User がプレイした Game エンティティをすべて取得することで、最終的なアクセスパターンを満たしました。このアクセスパターンを処理するため、転置インデックスパターンを使用してセカンダリインデックスを作成し、User エンティティと Game エンティティ間の多対多のリレーションシップの反対側でクエリを実行できるようにしました。
次のモジュールでは、作成したリソースをクリーンアップします。