前のモジュールでは、ゲームアプリケーションのアクセスパターンを定義しました。このモジュールでは、DynamoDB テーブルのプライマリキーを設計し、主要なアクセスパターンを有効にします。

モジュールの所要時間: 20 分


DynamoDB テーブルのプライマリキーを設計するときは、次のベストプラクティスを念頭に置いてください。

  • テーブルのさまざまなエンティティから開始します従業員、部門、顧客、注文など、複数の異なるタイプのデータを単一のテーブルに保存する場合、プライマリキーには各エンティティを明確に識別し、個々のアイテムのコアアクションを有効にする方法があることを確認してください。
  • プレフィックスを使用して、エンティティタイプを区別します。プレフィックスを使用してエンティティタイプを区別すると、衝突を防ぎ、クエリを支援できます。たとえば、同じテーブルに顧客と従業員の両方がある場合、顧客のプライマリキーは CUSTOMER#<CUSTOMERID> になります、従業員のプライマリキーは EMPLOYEE#<EMPLOYEEID> です。
  • 最初に単一項目のアクションに焦点を合わせ、可能な場合は複数項目のアクションを追加します。プライマリキーに関しては、GetItemPutItemUpdateItemDeleteItem の単一アイテム API を使用して、単一アイテムの読み取りおよび書き込みオプションを満たすことが重要です。Query を使用して、複数キーの読み取りパターンをプライマリキーで満たすこともできます。そうでない場合は、セカンダリインデックスを追加してユースケースの Query を処理できます。

これらのベストプラクティスを念頭に置いて、ゲームアプリケーションのテーブルのプライマリキーを設計し、いくつかの基本的なアクションを実行しましょう。


  • ステップ 1.プライマリキーを設計する

    上記の「はじめに」で触れたように、さまざまなエンティティを考えてみましょう。ゲームには、次のエンティティがあります。

    • User
    • Game
    • UserGameMapping

    UserGameMapping は、ユーザーがゲームに参加したことを示すレコードです。UserGame との間には多対多のリレーションシップがあります。

    通常、多対多のマッピングを持つことは、2 つのクエリパターンを満たすことを示すものであり、このゲームも例外ではありません。 ゲームに参加したすべてのユーザーを検索する必要があるアクセスパターンと、あるユーザーがプレイしたすべてのゲームを検索する別のパターンがあります。

    データモデルに複数のエンティティがあり、それらの間にリレーションシップがある場合、通常は複合プライマリキーHASHRANGE の両方の値で使用します。複合プライマリキーにより、HASH キーの Query 機能がもたらされ、必要なクエリパターンの 1 つを満たすことができます。DynamoDB のドキュメントでは、パーティションキーは HASH と呼ばれ、ソートキーは RANGE と呼ばれます。このガイドでは、特にコードまたは DynamoDB JSON ワイヤプロトコル形式について説明する場合、API 用語を同じ意味で用います。

    他の 2 つのデータエンティティ (UserGame) には、User または Game のアクセスパターンがキーと値のルックアップであるため、RANGE 値の自然なプロパティがありません。RANGE 値が必要なため、RANGE キーにフィラー値を指定できます。

    これを念頭に置いて、各エンティティタイプの HASH および RANGE 値に次のパターンを使用してみましょう。

    エンティティ HASH RANGE
    User USER#<USERNAME> #METADATA#<USERNAME>
    Game GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping GAME#<GAME_ID> USER#<USERNAME>

    上記のテーブルを見ていきましょう。

    User エンティティの場合、HASH 値は USER#<USERNAME> です。プレフィックスを使用してエンティティを識別し、エンティティタイプ間で発生する可能性のある衝突を防いでいます。

    User エンティティの RANGE の値には、#METADATA# の静的プレフィックスとそれに続く USERNAME 値を使用しています。RANGE 値では、USERNAME などの既知の値を持っていることが重要です。これにより、GetItemPutItemDeleteItem などの単一アイテムのアクションが可能になります。

    ただし、この列をインデックスの HASH キーとして使用する場合、均等パーティショニングを有効にするために、異なる User エンティティ全体で異なる値を持つ RANGE 値も必要です。そのため、USERNAME を追加します。

    Game エンティティには、User エンティティの設計に似たプライマリキーの設計があります。異なるプレフィックス (GAME#) と USERNAME の代わりに GAME_ID を使用しますが、原則は同じです。

    最後に、 UserGameMappingGame エンティティと同じ HASH キーを使用します。これにより、1 回のクエリで Game のメタデータだけでなく、Game のすべてのユーザーを取得できます。次に、UserGameMappingRANGE キーのUserエンティティを使用して、特定のゲームに参加したユーザーを識別します。

    次のステップでは、このプライマリキーのデザインを使用してテーブルを作成します。 

  • ステップ 2: テーブルを作成する

    プライマリキーを設計したので、テーブルを作成しましょう。

    モジュール 1 のステップ 3 でダウンロードしたコードには、create_table.py という名前の scripts/ ディレクトリーに Python スクリプトが含まれています。Python スクリプトの内容は次のとおりです。

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.create_table(
            TableName='battle-royale',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            KeySchema=[
                {
                    "AttributeName": "PK",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "SK",
                    "KeyType": "RANGE"
                }
            ],
            ProvisionedThroughput={
                "ReadCapacityUnits": 1,
                "WriteCapacityUnits": 1
            }
        )
        print("Table created successfully.")
    except Exception as e:
        print("Could not create table. Error:")
        print(e)
    

    上記のスクリプトは、AWS SDK for Python である Boto 3 を使用して、CreateTable オペレーションを使います。この操作では、プライマリキーで使用される型指定された属性である 2 つの属性定義を宣言します。DynamoDB はスキーマレスですが、プライマリキーに使用する属性の名前とタイプを宣言する必要があります。属性は、テーブルに書き込まれるすべてのアイテムに含める必要があるため、テーブルを作成するときに指定する必要があります。

    単一のテーブルに異なるエンティティを格納しているため、UserId などのプライマリキー属性名を使用できません。この属性は、保存するエンティティのタイプに基づいて異なる何かを意味します。たとえば、ユーザーのプライマリキーは USERNAME であり、ゲームのプライマリキーはその GAMEID であるかもしれません。したがって、PK (パーティションキー用) や SK (ソートキー用) などの一般的な名前を属性に使用します。

    キースキーマの属性を設定した後、プロビジョンドスループットをテーブルに指定します。DynamoDB には、プロビジョンドモードとオンデマンドの 2 つのキャパシティーモードがあります。プロビジョンドキャパシティーモードでは、必要な読み取りおよび書き込みスループットの量を正確に指定します。このキャパシティーは、使用するかどうかにかかわらずお支払いいただきます。

    DynamoDB オンデマンドのキャパシティーモードでは、リクエストごとに支払うことができます。リクエストごとのコストは、プロビジョニングされたスループットを完全に使用する場合よりもわずかに高くなりますが、キャパシティープランニングやスロットルの心配に時間を割く必要はありません。オンデマンドモードは、スパイキーなワークロードや予測不可能なワークロードに最適です。このラボでは、DynamoDB 無料利用枠に収まるため、プロビジョンドキャパシティーモードを使用しています。

    テーブルを作成するには、次のコマンドで Python スクリプトを実行します。

    python scripts/create_table.py

    スクリプトは、「テーブルが正常に作成されました」というメッセージを返すはずです。

    次のステップでは、いくつかのサンプルデータをテーブルに一括ロードします。 

  • ステップ 3: データをテーブルに一括ロードする

    このステップでは、前のステップで作成した DynamoDB に複数のデータを一括ロードします。これは、後のステップで使用するサンプルデータがあることを意味します。

    scripts/ ディレクトリに、items.json というファイルがあります。このファイルには、このラボ用にランダムに生成された 835 のサンプルアイテムが含まれています。これらのアイテムには、UserGameUserGameMapping エンティティが含まれます。サンプル項目の一部を表示する場合は、ファイルを開きます。

    scripts/ ディレクトリには、bulk_load_table.py というファイルもあります。このファイルは、items.json ファイル内のアイテムを読み取り、DynamoDB テーブルに一括書き込みします。そのファイルの内容が続きます。

    import json
    
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('battle-royale')
    
    items = []
    
    with open('scripts/items.json', 'r') as f:
        for row in f:
            items.append(json.loads(row))
    
    with table.batch_writer() as batch:
        for item in items:
            batch.put_item(Item=item)
    

    このスクリプトでは、Boto 3 で低レベルクライアントを使用するのではなく、高レベル Resource オブジェクトを使用します。Resource オブジェクトは、AWS API を使用するための簡単なインターフェイスを提供します。Resource オブジェクトは、リクエストをバッチ処理するため、この状況で役立ちます。BatchWriteItem 操作は、1 つのリクエストで最大 25 個のアイテムを受け入れます。Resource オブジェクトは、25 個以下のアイテムのリクエストにデータを分割するのではなく、バッチ処理を行います。

    bulk_load_table.py スクリプトを実行し、ターミナルで次のコマンドを実行して、テーブルにデータをロードします。

    python scripts/bulk_load_table.py

    Scan 操作を実行して集計結果を表示することにより、すべてのデータがテーブルにロードされたことを確認できます。

    aws dynamodb scan \
     --table-name battle-royale \
     --select COUNT

    これにより、次の結果が表示されます。

    {
        "Count": 835, 
        "ScannedCount": 835, 
        "ConsumedCapacity": null
    }
    

    835 の Count が表示され、すべてのアイテムが正常にロードされたことを示します。

    次のステップでは、1 つのリクエストで複数のエンティティタイプを取得する方法を示します。これにより、アプリケーションで行うネットワークリクエストの総数を減らし、アプリケーションのパフォーマンスを向上させることができます。

  • ステップ 4: 1 つのリクエストで複数のエンティティタイプを取得する

    前のモジュールで述べたように、DynamoDB テーブルは受信するリクエストの数に合わせて最適化する必要があります。また、DynamoDB には、リレーショナルデータベースにあるジョインはありません。代わりに、リクエストでジョインするような動作を許可するようにテーブルを設計します。

    このステップでは、1 つのリクエストで複数のエンティティタイプを取得します。ゲームでは、ゲームセッションに関する詳細を取得できます。これらの詳細には、開始時刻、終了時刻、ゲームの参加者、ゲームでプレイしたユーザーに関する詳細など、ゲーム自体に関する情報が含まれます。

    このリクエストは、Game エンティティと UserGameMapping エンティティの 2 つのエンティティタイプにまたがっています。ただし、これは複数のリクエストを行う必要があるという意味ではありません。

    ダウンロードしたコードでは、fetch_game_and_players.py スクリプトは application/ ディレクトリにあります。このスクリプトは、単一のリクエストでゲームの Game エンティティと UserGameMapping エンティティの両方を取得するコードを構築する方法を示しています。

    次のコードは、fetch_game_and_players.py スクリプトを構成します。

    import boto3
    
    from entities import Game, UserGameMapping
    
    dynamodb = boto3.client('dynamodb')
    
    GAME_ID = "3d4285f0-e52b-401a-a59b-112b38c4a26b"
    
    
    def fetch_game_and_users(game_id):
        resp = dynamodb.query(
            TableName='battle-royale',
            KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :users",
            ExpressionAttributeValues={
                ":pk": { "S": "GAME#{}".format(game_id) },
                ":metadata": { "S": "#METADATA#{}".format(game_id) },
                ":users": { "S": "USER$" },
            },
            ScanIndexForward=True
        )
    
        game = Game(resp['Items'][0])
        game.users = [UserGameMapping(item) for item in resp['Items'][1:]]
    
        return game
    
    
    game = fetch_game_and_users(GAME_ID)
    
    print(game)
    for user in game.users:
        print(user)
    

    このスクリプトの冒頭で、Boto 3 ライブラリといくつかの単純なクラスをインポートし、アプリケーションコード内のオブジェクトを表します。これらのエンティティの定義は、application/entities.py ファイルで確認できます。

    スクリプトの実際の作業は、モジュールで定義されている fetch_game_and_users 関数で行われます。これは、このデータを必要とするエンドポイントが使用するアプリケーションで定義する関数に似ています。

    fetch_game_and_users 関数はいくつかのことを行います。まず、DynamoDB に対して Query リクエストを行います。この Query は、GAME#<GameId>PK を使用します。次に、ソートキーが #METADATA#<GameId> USER$ の間にあるエンティティをリクエストします。これにより、ソートキーが #METADATA#<GameId> である Game エンティティ、およびキーが USER# で始まるすべての UserGameMappings エンティティを取得します。文字列タイプのソートキーは、ASCII 文字コードでソートされます。ASCII のドル記号 ($) はポンド記号 (#) の直後にあるため、UserGameMapping エンティティですべてのマッピングを確実に取得できます。

    応答を受信すると、データエンティティをアプリケーションが認識するオブジェクトに組み立てます。返される最初のエンティティは Game エンティティであることがわかっているため、このエンティティから Game オブジェクトを作成します。残りのエンティティについては、各エンティティに対して UserGameMapping オブジェクトを作成し、ユーザーの配列を Game オブジェクトにアタッチします。

    スクリプトの最後には、関数の使用法が表示され、結果のオブジェクトが出力されます。次のコマンドを使用して、ターミナルでスクリプトを実行できます。

    python application/fetch_game_and_players.py

    スクリプトは、Game オブジェクトとすべての UserGameMapping オブジェクトをコンソールに出力する必要があります。

    Game<3d4285f0-e52b-401a-a59b-112b38c4a26b -- Green Grasslands>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- branchmichael>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- deanmcclure>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emccoy>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- emma83>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- iherrera>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- jeremyjohnson>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- lisabaker>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- maryharris>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- mayrebecca>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- meghanhernandez>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- nruiz>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- pboyd>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- richardbowman>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- roberthill>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- robertwood>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- victoriapatrick>
    UserGameMapping<3d4285f0-e52b-401a-a59b-112b38c4a26b -- waltervargas>
    

    このスクリプトは、1 つの DynamoDB リクエストで複数のエンティティタイプを取得するために、テーブルをモデル化し、クエリを記述する方法を示しています。リレーショナルデータベースでは、ジョインを使用して、1 つのリクエストで異なるテーブルから複数のエンティティタイプを取得します。DynamoDB では、一緒にアクセスする必要のあるエンティティが 1 つのテーブル内で隣り合って配置されるように、データを具体的にモデル化します。このアプローチは、一般的なリレーショナルデータベースでジョインを不要にし、スケールアップ時にアプリケーションのパフォーマンスを維持します。


    このモジュールでは、プライマリキーを設計し、テーブルを作成しました。次に、データをテーブルに一括ロードし、1 つのリクエストで複数のエンティティタイプをクエリする方法を確認しました。

    現在のプライマリキーの設計では、次のアクセスパターンを満たすことができます。

    • ユーザープロファイルを作成 (書き込み)
    • ユーザープロファイルを更新 (書き込み)
    • ユーザープロファイルを取得 (読み取り)
    • ゲームを作成 (書き込み)
    • ゲームを見る (読み取り)
    • ユーザーがゲームに参加 (書き込み)
    • ゲームを開始 (書き込み)
    • ユーザーのゲームを更新 (書き込み)
    • ゲームを更新 (書き込み)

    次のモジュールでは、セカンダリインデックスを追加し、スパースインデックス手法について学習します。セカンダリインデックスにより、DynamoDB テーブルで追加のアクセスパターンをサポートできます。