在上一個單元中,我們定義了遊戲應用程式的存取模式。在此單元中,我們將設計 DynamoDB 資料表的主要金鑰並啟用核心存取模式。

完成單元的時間:20 分鐘


在為 DynamoDB 資料表設計主要金鑰時,請牢記以下最佳實務:

  • 從資料表中的不同實體開始。若您要在單一資料表中儲存多種不同類型的資料 (例如,員工、部門、客戶和訂單),請確保您的主要金鑰可以區分每個實體,並針對個別項目啟用核心動作。
  • 使用前綴來區分實體類型。使用前綴來區分實體類型可以防止衝突並有助於查詢。例如,若您在相同資料表中同時有客戶和雇員,則客戶的主要金鑰可以是 CUSTOMER#<CUSTOMERID>,而雇員的主要金鑰可以是 EMPLOYEE#<EMPLOYEEID>。
  • 首先關注單項操作,然後在可能的情況下新增多項操作。對於主要金鑰,務必使用單項 API 來滿足對單項的讀取和寫入選項:GetItemPutItemUpdateItemDeleteItem。透過使用查詢,您還可以使用主要金鑰來滿足您的多項讀取模式。若不是,則可新增輔助索引來處理查詢使用案例。

考慮到這些最佳實務,我們來設計遊戲應用程式資料表的主要金鑰,並執行一些基本操作。


  • 步驟 1.設計主要金鑰

    我們來考慮一下前述介紹中建議的不同實體。在遊戲中,我們具有以下實體:

    • User
    • Game
    • UserGameMapping

    UserGameMapping 是表示使用者加入遊戲的記錄。UserGame 之間存在多對多關係。

    通常,具有多對多映射表示您要滿足兩種查詢模式,而此遊戲也不例外。 一種模式用於尋找所有已加入遊戲的使用者,另一種模式用於尋找使用者玩的所有遊戲。

    若您的資料模型具有多個相互關聯的實體,則通常使用具有 HASHRANGE 值的複合主要金鑰。複合主要金鑰為我們提供了 HASH 金鑰上的查詢功能,可以滿足我們所需的一種查詢模式。在 DynamoDB 文件中,分區金鑰稱為 HASH,排序金鑰稱為 RANGE,在本指南中,我們可以互換使用 API ​​術語,在討論程式碼或 DynamoDB JSON 通訊協定格式時尤其如此。

    其他兩個資料實體——UserGame 對於 RANGE 值不具有自然屬性,因為 User Game 為金鑰值查詢。因為需要 RANGE 值,所以我們可以為 RANGE 金鑰提供填充值。

    考慮到這一點,讓我們對每種實體類型的 HASHRANGE 值使用以下模式。

    實體 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 金鑰,我們還需要 RANGE 值在不同的 User 實體中具有不同的值,以啟用均勻分割。因此,我們附加了 USERNAME

    Game 實體的主要金鑰設計與 User 實體的設計相似。它使用不同的前綴 (GAME#) 和 GAME_ID,而不是 USERNAME,但原理相同。

    最後,UserGameMapping 使用與 Game 實體相同的 HASH 金鑰。這讓我們不僅能夠在單一查詢中擷取 Game 的中繼資料,還可以擷取 Game 中的所有使用者。然後,我們將 UserGameMappingRANGE 金鑰的 User 實體用於識別哪個使用者已加入特定遊戲。

    在下一步中,我們使用此主要金鑰設計來建立資料表。 

  • 步驟 2:建立資料表

    現在,我們已經設計了主要金鑰,接下來建立資料表。

    您在單元 1 步驟 3 中下載的程式碼在名稱為 create_table.pyscripts/ 目錄中包含一個 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)
    

    上述指令碼透過 Boto 3 (適用於 Python 的 AWS 開發套件) 來使用 CreateTable 操作。該操作聲明兩個屬性定義,它們是要在主要金鑰中使用的類型化屬性。雖然 DynamoDB 是 schemaless,但是您必須聲明用於主要金鑰的名稱和屬性類型。屬性必須包含在寫入資料表的每個項目中,因此在建立資料表時必須指定屬性。

    由於我們將不同的實體儲存在單一資料表中,因此不能使用主要金鑰屬性名稱,例如 UserId。該屬性根據儲存的實體類型而有所差異。例如,使用者的主要金鑰可能是其 USERNAME,而遊戲的主要金鑰可能是其 GAMEID。因此,我們對屬性使用通用名稱,例如 PK (用於分割金鑰) 和 SK (用於排序金鑰)。

    在金鑰結構描述中設定屬性後,我們為資料表指定佈建輸送量。DynamoDB 具有兩種容量模式:佈建和隨需。在佈建容量模式下,您可以準確指定所需的讀寫輸送量。無論是否使用,都需要為此容量付費。

    在 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 objectResource 物件提供更簡單的界面來使用 AWS API 。Resource 物件在這種情況下很有用,因為它可以大量處理我們的請求。BatchWriteItem 操作在單一請求中最多接受 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,表明所有項目均已成功載入。

    在下一步中,我們將展示如何在單一請求中擷取多種實體類型,這可減少您在應用程式中的網路請求總數,並提高應用程式效能。

  • 步驟 4:在單一請求中擷取多種實體類型

    正如我們在上一單元中所述,您應針對 DynamoDB 資料表所接收的請求數量進行最佳化。我們還提到 DynamoDB 沒有關聯式資料庫所具備的聯接。而是,您可以設計資料表,以允許您的請求中具有類似聯接的行為。

    在此步驟中,我們在單一請求中擷取多種實體類型。在遊戲中,我們可能想擷取有關遊戲工作階段的詳細資訊。這些詳細資訊包括有關遊戲本身的資訊,例如開始時間、結束時間、放置者,以及有關遊戲玩家的詳細資訊。

    該請求跨越兩種實體類型:Game 實體和 UserGameMapping 實體。但是,這並不意味著我們需要提出多個請求。

    在您下載的程式碼中,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 發出查詢請求。此查詢使用 GAME#<GameId> 中的 PK。然後,請求排序金鑰在 #METADATA#<GameId>USER$ 之間的任何實體。這將擷取 Game 實體 (其排序金鑰為 #METADATA#<GameId>),以及所有 UserGameMappings 實體,其金鑰以 USER# 開頭。字串類型的排序金鑰按 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>
    

    此指令碼顯示如何在單一 DynamoDB 請求中對資料表建模,並編寫查詢以擷取多種實體類型。在關聯式資料庫中,您可以在單一請求中使用聯接,以從不同資料表中擷取多種實體類型。使用 DynamoDB,您可以專門為資料建模,以便將應當一起存取的實體在單一資料表中彼此相鄰。這種方法取代了在典型的關聯式資料庫中進行聯接的需求,並在您擴展應用程式時保持了高效能。


    在此單元中,我們設計了主要金鑰並建立了資料表。然後,我們將資料大量載入至資料表中,並了解了如何在單一請求中查詢多種實體類型。

    使用目前的主要金鑰設計,我們可以滿足以下存取模式:

    • 建立使用者檔案 (寫入)
    • 更新使用者檔案 (寫入)
    • 取得使用者檔案 (讀取)
    • 建立遊戲 (寫入)
    • 檢視遊戲 (讀取)
    • 將使用者加入遊戲 (寫入)
    • 開始遊戲 (寫入)
    • 更新使用者的遊戲 (寫入)
    • 更新遊戲 (寫入)

    在下一個單元中,我們將新增次要索引並了解稀疏索引技術。次要索引讓您能夠支援 DynamoDB 資料表上的其他存取模式。