Trong mô-đun trước, chúng ta đã xác định các mẫu hình truy cập của ứng dụng trò chơi. Trong mô-đun này, chúng ta sẽ thiết kế khóa chính cho bảng DynamoDB và kích hoạt các mẫu hình truy cập cốt lõi.

Thời gian hoàn thành mô-đun: 20 phút


Khi thiết kế khóa chính cho bảng DynamoDB, hãy ghi nhớ các biện pháp tốt nhất sau đây:

  • Bắt đầu với các thực thể khác nhau trong bảng của bạn. Nếu bạn đang lưu trữ nhiều loại dữ liệu khác nhau trong một bảng—chẳng hạn như nhân viên, phòng ban, khách hàng và đơn hàng—hãy chắc chắn rằng khóa chính của bạn có cách để xác định rõ ràng từng thực thể và kích hoạt các thao tác cốt lõi trên từng mục riêng lẻ.
  • Sử dụng tiền tố để phân biệt giữa các loại thực thể. Việc sử dụng tiền tố để phân biệt giữa các loại thực thể có thể ngăn ngừa xung đột và hỗ trợ truy vấn. Ví dụ: Nếu bạn có cả khách hàng và nhân viên trong cùng một bảng, khóa chính cho khách hàng có thể là CUSTOMER#<CUSTOMERID> và khóa chính cho nhân viên có thể là EMPLOYEE#<EMPLOYEEID>.
  • Trước tiên, hãy tập trung vào các thao tác đơn mục, sau đó thêm các thao tác đa mục, nếu có thể. Đối với khóa chính, quan trọng là bạn có thể đáp ứng các tùy chọn đọc và ghi trên một mục bằng cách sử dụng API một mục: GetItem, PutItem, UpdateItemDeleteItem. Bạn cũng có thể đáp ứng các mẫu hình đọc nhiều mục với khóa chính bằng cách sử dụng Query. Nếu không, bạn có thể thêm chỉ mục phụ để xử lý các trường hợp sử dụng Query.

Với các biện pháp tốt nhất này, hãy cùng thiết kế khóa chính cho bảng trong ứng dụng trò chơi và thực hiện một số thao tác cơ bản.


  • Bước 1. Thiết kế khóa chính

    Hãy xem xét các thực thể khác nhau, như được đề xuất trong phần giới thiệu trước. Trong trò chơi, chúng ta có các thực thể sau:

    • User
    • Game
    • UserGameMapping

    UserGameMapping là bản ghi cho biết một người dùng đã tham gia trò chơi. Có mối quan hệ nhiều - nhiều giữaUserGame.

    Việc có ánh xạ nhiều - nhiều thường là dấu hiệu cho thấy bạn muốn đáp ứng hai mẫu hình Query và trò chơi này cũng không phải là ngoại lệ. Chúng ta có một mẫu hình truy cập cần tìm tất cả người dùng đã tham gia trò chơi, đồng thời có một mẫu khác để tìm tất cả trò chơi mà người dùng đã chơi.

    Nếu mô hình dữ liệu của bạn có nhiều thực thể với mối quan hệ giữa các thực thể, bạn thường sử dụng khóa chính tổng hợp với cả hai giá trị HASHRANGE. Khóa chính tổng hợp cung cấp cho chúng ta khả năng Query trên khóa HASH để đáp ứng một trong các mẫu hình truy vấn mà chúng ta cần. Trong tài liệu về DynamoDB, khóa phân vùng được gọi là HASH và khóa sắp xếp được gọi là RANGE. Trong hướng dẫn này, chúng tôi sử dụng thuật ngữ API thay thế tương đương, đặc biệt là khi thảo luận về mã hoặc định dạng giao thức có dây JSON của DynamoDB.

    Hai thực thể dữ liệu khác—User Game—không có thuộc tính tự nhiên cho giá trị RANGE vì các mẫu hình truy cập trên Người dùng hoặc Game là tra cứu giá trị khóa. Vì giá trị RANGE là bắt buộc nên chúng ta có thể cung cấp giá trị đệm cho khóa RANGE.

    Khi đã nắm được điều này, hãy sử dụng mẫu hình sau cho các giá trị HASHRANGE cho mỗi loại thực thể.

    Thực thể HASH RANGE
    User USER#<USERNAME> #METADATA#<USERNAME>
    Game GAME#<GAME_ID> #METADATA#<GAME_ID>
    UserGameMapping GAME#<GAME_ID> USER#<USERNAME>

    Hãy cùng xem xét chi tiết của bảng trước.

    Đối với thực thể User, giá trị HASH USER#<USERNAME>. Lưu ý rằng chúng ta đang sử dụng tiền tố để xác định thực thể và ngăn chặn mọi xung đột có thể xảy ra giữa các loại thực thể.

    Đối với giá trị RANGE trên thực thể User, chúng ta sử dụng tiền tố tĩnh #METADATA# với giá trị USERNAME theo sau. Đối với giá trị RANGE, chúng ta cần có một giá trị đã xác định, chẳng hạn như USERNAME. Điều này cho phép thực hiện các thao tác đơn mục như GetItem, PutItemDeleteItem.

    Tuy nhiên, chúng ta cũng muốn có giá trị RANGE với các giá trị khác nhau trên các thực thể User khác nhau để cho phép phân vùng đồng đều trong trường hợp ta sử dụng cột này làm khóa HASH cho một chỉ mục. Vì lý do đó, chúng ta bổ sung thêm USERNAME.

    Thực thể Game có thiết kế khóa chính tương tự như thiết kế của thực thể User. Thực thể này sử dụng một tiền tố khác (GAME#) và GAME_ID thay vì USERNAME nhưng nguyên tắc là như nhau.

    Cuối cùng, UserGameMapping sử dụng cùng khóa HASH với thực thể Game. Điều này cho phép chúng ta không chỉ tải siêu dữ liệu về Game mà còn về tất cả người dùng trong Game ở một truy vấn duy nhất. Sau đó, chúng ta sử dụng thực thể User cho khóa RANGE trên UserGameMapping để xác định người dùng nào đã tham gia một trò chơi cụ thể.

    Trong bước tiếp theo, chúng ta sẽ tạo bảng với thiết kế khóa chính này. 

  • Bước 2: Tạo bảng

    Chúng ta đã thiết kế xong khóa chính, giờ thì hãy cùng tạo bảng.

    Mã bạn đã tải xuống trong Bước 3 của Mô-đun 1 bao gồm tập lệnh Python trong thư mục scripts/ có tên là created_table.py. Sau đây là nội dung của tập lệnh 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)
    

    Tập lệnh trước sử dụng thao tác CreateTable bằng Boto 3, AWS SDK cho Python. Thao tác này khai báo hai định nghĩa thuộc tính, là các thuộc tính theo loại sẽ được sử dụng trong khóa chính. Mặc dù DynamoDB không cần lược đồ, bạn vẫn phải khai báo tên và loại thuộc tính được sử dụng cho khóa chính. Các thuộc tính này phải có trong tất cả các mục được ghi vào bảng, do đó phải được chỉ định khi bạn tạo bảng.

    Vì chúng ta đang lưu trữ các thực thể khác nhau trong một bảng nên ta không thể sử dụng các tên thuộc tính khóa chính như UserId. Thuộc tính là nội dung sẽ thay đổi dựa trên loại thực thể được lưu trữ. Ví dụ: Khóa chính cho người dùng có thể là USERNAME của người dùng đó và khóa chính cho trò chơi có thể là GAMEID của trò chơi đó. Theo đó, chúng ta sử dụng tên chung cho các thuộc tính, chẳng hạn như PK (cho khóa phân vùng) và SK (cho khóa sắp xếp).

    Sau khi đặt cấu hình cho các thuộc tính trong lược đồ khóa, chúng ta sẽ chỉ định thông lượng được cung cấp cho bảng. DynamoDB có hai chế độ dung lượng: được cung cấp và theo nhu cầu. Trong chế độ dung lượng được cung cấp, bạn chỉ định chính xác lượng thông lượng đọc và ghi mong muốn. Bạn phải thanh toán cho dung lượng này dù có sử dụng hay không.

    Trong chế độ dung lượng theo nhu cầu của DynamoDB, bạn có thể trả tiền cho mỗi yêu cầu. Chi phí cho mỗi yêu cầu cao hơn một chút so với việc bạn sử dụng hết thông lượng được cung cấp nhưng bạn không phải dành thời gian để lập kế hoạch sử dụng dung lượng hoặc lo lắng về việc bị điều chỉnh. Chế độ theo nhu cầu phù hợp với khối lượng công việc tăng đột biến hoặc không thể dự đoán trước. Chúng ta sử dụng chế độ dung lượng được cung cấp trong phòng thực hành này vì chế độ này phù hợp với bậc miễn phí của DynamoDB.

    Để tạo bảng, hãy chạy tập lệnh Python bằng lệnh sau.

    python scripts/create_table.py

    Tập lệnh sẽ trả về thông báo này: “Table created successfully” (Đã tạo bảng thành công).

    Trong bước tiếp theo, chúng ta sẽ tải hàng loạt một số dữ liệu mẫu vào bảng. 

  • Bước 3: Tải hàng loạt dữ liệu vào bảng

    Trong bước này, chúng ta sẽ tải hàng loạt một số dữ liệu vào DynamoDB mà chúng ta đã tạo ở bước trước. Điều này có nghĩa là trong các bước tiếp theo, chúng ta sẽ có dữ liệu mẫu để sử dụng.

    Trong thư mục scripts/, bạn sẽ tìm thấy một tệp có tên items.json. Tệp này chứa 835 mục ví dụ được tạo ngẫu nhiên cho phòng thực hành này. Các mục này bao gồm các thực thể User, GameUserGameMapping. Mở tệp nếu bạn muốn xem một số mục ví dụ.

    Thư mục scripts/ cũng có một tệp tên là bulk_load_table.py, tệp này sẽ đọc các mục trong tệp items.json và ghi hàng loạt các mục này vào bảng DynamoDB. Sau đây là nội dung của tệp.

    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)
    

    Trong tập lệnh này, thay vì sử dụng máy khách cấp thấp trong Boto 3, chúng ta sử dụng đối tượng Resource cấp cao hơn. Các đối tượng Resource cung cấp giao diện dễ sử dụng API AWS hơn. Đối tượng Resource rất hữu ích trong tình huống này vì đối tượng này sẽ đóng các yêu cầu của chúng ta thành lô. Thao tác BatchWriteItem chấp nhận tối đa 25 mục trong một yêu cầu. Đối tượng Resource xử lý việc tạo lô đó cho chúng ta thay vì bắt chúng ta chia dữ liệu thành các yêu cầu có 25 mục trở xuống.

    Chạy tập lệnh bulk_load_table.py và tải bảng chứa dữ liệu của bạn bằng cách chạy lệnh sau trong terminal.

    python scripts/bulk_load_table.py

    Bạn có thể đảm bảo rằng tất cả dữ liệu đã được tải vào bảng bằng cách chạy thao tác Scan và xem số lượng.

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

    Hoạt động này sẽ hiển thị các kết quả sau đây.

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

    Bạn sẽ thấy Count là 835, cho biết tất cả các mục của bạn đã được tải thành công.

    Trong bước tiếp theo, chúng tôi sẽ trình bày cách truy xuất nhiều loại thực thể trong một yêu cầu, điều này có thể làm giảm tổng số yêu cầu mạng bạn tạo trong ứng dụng và nâng cao hiệu suất ứng dụng.

  • Bước 4: Truy xuất nhiều loại thực thể trong một yêu cầu

    Như chúng tôi đã nói trong mô-đun trước, bạn nên tối ưu hóa các bảng DynamoDB cho số lượng yêu cầu mà bảng này nhận được. Chúng tôi cũng đã đề cập rằng DynamoDB không có các kết nối mà cơ sở dữ liệu quan hệ có. Thay vào đó, bạn sẽ thiết kế bảng để cho phép thực hiện hành vi giống như kết nối trong các yêu cầu.

    Trong bước này, chúng ta sẽ truy xuất nhiều loại thực thể trong một yêu cầu. Trong trò chơi, chúng ta có thể muốn tải thông tin chi tiết về phiên trò chơi. Những chi tiết này bao gồm thông tin về chính trò chơi như thời gian bắt đầu, thời gian kết thúc, người đạt thứ hạng cao trong trò chơi và thông tin chi tiết về người dùng đã chơi trong trò chơi.

    Yêu cầu này bao trùm hai loại thực thể: thực thể Game và thực thể UserGameMapping. Tuy nhiên, điều này không có nghĩa là chúng ta cần thực hiện nhiều yêu cầu.

    Trong mã bạn đã tải xuống, tập lệnh fetch_game_and_players.py nằm trong thư mục application/. Tập lệnh này cho thấy cách bạn có thể cấu trúc mã của mình để truy xuất cả thực thể Game và thực thể UserGameMapping cho trò chơi trong một yêu cầu.

    Mã sau là nội dung của tập lệnh 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)
    

    Ở đầu tập lệnh này, chúng ta nhập thư viện Boto 3 và một số lớp đơn giản để thể hiện các đối tượng trong mã ứng dụng của mình. Bạn có thể xem định nghĩa cho các thực thể đó trong tệp application/entities.py.

    Tác dụng chủ yếu của tập lệnh nằm ở hàm fetch_game_and_users được định nghĩa trong mô-đun. Hàm này tương tự như hàm bạn sẽ định nghĩa trong ứng dụng để phục vụ cho mọi điểm cuối cần dữ liệu này.

    Hàm fetch_game_and_users thực hiện một số hoạt động. Trước hết, hàm này tạo yêu cầu Query tới DynamoDB. Query này sử dụng PK của GAME#<GameId>. Sau đó, hàm này yêu cầu mọi thực thể có khóa sắp xếp nằm trong khoảng #METADATA#<GameId> USER$. Hàm này sẽ lấy thực thể Trò chơi có khóa sắp xếp là #METADATA#<GameId> và tất cả thực thể UserGameMappings có khóa bắt đầu bằng USER#. Khóa sắp xếp của loại chuỗi này được sắp xếp theo mã ký tự ASCII. Ký hiệu đô la ($) xuất hiện ngay sau ký hiệu dấu thăng (#) trong ASCII, do đó, điều này đảm bảo rằng chúng ta sẽ nhận được tất cả ánh xạ trong thực thể UserGameMapping.

    Khi nhận được phản hồi, chúng ta sẽ tập hợp các thực thể dữ liệu của mình thành các đối tượng mà ứng dụng đã biết. Chúng ta biết rằng thực thể đầu tiên được trả về là thực thể Game, vì vậy chúng ta sẽ tạo đối tượng Game từ thực thể này. Đối với các thực thể còn lại, chúng ta tạo đối tượng UserGameMapping cho từng thực thể, rồi đính kèm mảng người dùng vào đối tượng Game.

    Phần cuối của tập lệnh sẽ hiển thị việc sử dụng hàm và in ra các đối tượng kết quả. Bạn có thể chạy tập lệnh này với lệnh sau trong terminal của mình.

    python application/fetch_game_and_players.py

    Tập lệnh sẽ in đối tượng Game và tất cả các đối tượng UserGameMapping vào bảng điều khiển.

    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>
    

    Tập lệnh này cho thấy cách bạn có thể lập mô hình cho bảng của mình và viết các truy vấn để truy xuất nhiều loại thực thể trong một yêu cầu DynamoDB. Trong cơ sở dữ liệu quan hệ, bạn sử dụng phép kết nối để truy xuất nhiều loại thực thể từ các bảng khác nhau trong một yêu cầu. Với DynamoDB, bạn lập mô hình cụ thể cho dữ liệu của mình để các thực thể bạn sẽ truy cập đồng thời được đặt cạnh nhau trong một bảng. Cách tiếp cận này thay thế nhu cầu cần tạo kết nối trong cơ sở dữ liệu quan hệ điển hình và giữ cho ứng dụng của bạn có hiệu suất cao khi bạn mở rộng quy mô.


    Trong mô-đun này, chúng ta đã thiết kế khóa chính và tạo bảng. Sau đó, chúng ta đã tải hàng loạt dữ liệu vào bảng và xem cách truy vấn nhiều loại thực thể trong một yêu cầu.

    Với thiết kế khóa chính hiện tại, chúng ta có thể đáp ứng các mẫu hình truy cập sau:

    • Tạo hồ sơ người dùng (Ghi)
    • Cập nhật hồ sơ người dùng (Ghi)
    • Tải hồ sơ người dùng (Đọc)
    • Tạo trò chơi (Ghi)
    • Xem trò chơi (Đọc)
    • Tham gia trò chơi cho người dùng (Ghi)
    • Bắt đầu trò chơi (Ghi)
    • Cập nhật trò chơi cho người dùng (Ghi)
    • Cập nhật trò chơi (Ghi)

    Trong mô-đun tiếp theo, chúng ta sẽ thêm chỉ mục phụ và tìm hiểu về kỹ thuật chỉ mục thưa. Chỉ mục phụ cho phép bạn hỗ trợ các mẫu hình truy cập bổ sung trên bảng DynamoDB.