В предыдущем модуле мы определили шаблоны доступа к игровому приложению. В этом модуле мы разработаем первичный ключ для таблицы DynamoDB и включим базовые шаблоны доступа.

Время, необходимое для прохождения модуля: 20 минут


При разработке первичного ключа для таблицы DynamoDB следует учитывать следующие рекомендации:

  • Начните с разных сущностей в таблице. Если вы храните несколько разных типов данных в одной таблице, например сотрудников, отделы, клиентов и заказы, убедитесь, что первичный ключ может четко идентифицировать каждую сущность и выполнять основные действия для отдельных элементов.
  • Используйте префиксы, чтобы различать типы сущностей. Использование префиксов для различения типов сущностей поможет предотвратить конфликты и упростить обработку запросов. Например, если в одной таблице содержатся клиенты и сотрудники, первичным ключом для клиента может быть CUSTOMER#<CUSTOMERID>, а для сотрудника – EMPLOYEE#<EMPLOYEEID>.
  • Сначала уделите внимание действиям с одним элементом, а затем, если возможно, добавьте многоэлементные действия. При создании первичного ключа необходимо соблюдать параметры чтения и записи для одного элемента, используя одноэлементные API: GetItem, PutItem, UpdateItem и DeleteItem. Вы также можете учесть требования шаблонов чтения первичным ключом нескольких элементов, используя Query. В противном случае можно добавить вторичный индекс для обработки примеров использования Query.

Давайте разработаем первичный ключ для таблицы игрового приложения и выполним некоторые основные действия, используя данные рекомендации.


  • Шаг 1. Разработка первичного ключа

    Давайте рассмотрим различные сущности, описанные во введении. В игре используются следующие сущности:

    • User
    • Game
    • UserGameMapping

    UserGameMapping – это запись, которая указывает, что пользователь присоединился к игре. Между User и Game установлены отношения «многие ко многим».

    Наличие сопоставления «многие ко многим» обычно подразумевает соответствие двум шаблона Query, как и в этой игре. У нас есть шаблон доступа, который находит всех пользователей, присоединившихся к игре, а также шаблон, который находит все игры, в которые играл пользователь.

    Если в вашу модель данных входит несколько сущностей со взаимосвязями, обычно следует использовать сложный первичный ключ со значениями HASH и RANGE. Сложный первичный ключ позволяет использовать Query с ключом HASH, что позволяет соответствовать одному из требуемых шаблонов запроса. В документации DynamoDB ключ секции называется HASH, а ключ сортировки – RANGE. В этом модуле термины API используются взаимозаменяемо, особенно при обсуждении кода или формата протокола передачи данных JSON DynamoDB.

    Две других сущности данных (User и Game) не имеют естественного свойства для значения RANGE, поскольку шаблоны доступа для User или Game основаны на поиске пар «ключ-значение». Поскольку требуется значение 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>. Обратите внимание, что мы используем префикс для идентификации сущности и предотвращения возможных конфликтов между типами сущностей.

    Для значения RANGE в сущности User используется статический префикс #METADATA#, за которым следует значение USERNAME. Для значения RANGE важно иметь известное значение, например USERNAME. Это позволяет выполнять отдельные действия, например GetItem, PutItem и DeleteItem.

    Однако для обеспечения равного разбиения также требуется, чтобы при использовании этого столбца в качестве ключа HASH для индекса значение RANGE менялось в разных сущностях User. По этой причине мы добавляем USERNAME.

    Сущность Game имеет схему первичного ключа, аналогичную схеме сущности User. Вместо USERNAME она использует другой префикс (GAME#) и GAME_ID, но общие принципы остаются неизменны.

    Наконец, UserGameMapping использует тот же ключ HASH, что и сущность Game. Это позволяет получать метаданные не только для Game, но и всех пользователей Game в одном запросе. Затем мы используем сущность User для ключаRANGE в UserGameMapping, чтобы определить, какой пользователь присоединился к определенной игре.

    В следующем шаге будет создана таблица с этой схемой первичного ключа. 

  • Шаг 2. Создание таблицы

    Теперь, завершив разработку первичного ключа, давайте создадим таблицу.

    Код, загруженный в шаге 3 модуля 1, содержит скрипт Python под названием create_table.py в каталоге scripts/. Содержание скрипта 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)
    

    В предыдущем скрипте применяется операция CreateTable с использованием Boto 3, AWS SDK для Python. Операция объявляет два определения атрибута, которые являются типизированными атрибутами и будут использоваться в первичном ключе. Хотя DynamoDB работаетбез схем, нам необходимо объявить имена и типы атрибутов, которые используются в первичных ключах. Атрибуты следует включить в каждый элемент, который записывается в таблицу, поэтому они указываются при создании таблицы.

    Поскольку мы храним разные сущности в одной таблице, то не можем использовать имена атрибутов первичного ключа, например UserId. В зависимости от типа сохраняемой сущности атрибут может иметь разные функции. Например, первичным ключом пользователя может быть его USERNAME, а первичным ключом игры – ее GAMEID. Соответственно, для атрибутов используются общие имена, например PK (для ключа секции) и SK (для ключа сортировки).

    После настройки атрибутов в схеме ключа мы указываем выделенный объем пропускной способности для таблицы. DynamoDB имеет два режима пропускной способности: резервный режим и режим по требованию. В режиме выделенных ресурсов вы указываете только необходимый объем пропускной способности чтения и записи. Вы платите за данную пропускную способность, даже если не используете ее.

    В режиме пропускной способности DynamoDB по требованию можно платить за запросы. Стоимость каждого запроса немного выше, чем при полном использовании выделенного объема пропускной способности, но при этом вам не понадобится тратить время на планирование или беспокоиться об ограничении пропускной способности. Режим по требованию отлично подходит для резких скачков и непредсказуемых рабочих нагрузок. В этом модуле мы используем режим выделенных ресурсов, поскольку он соответствует уровню бесплатного пользования DynamoDB.

    Чтобы создать таблицу, запустите скрипт Python с помощью следующей команды.

    python scripts/create_table.py

    Скрипт должен вернуть такое сообщение: «Table created successfully».

    В следующем этапе мы загружаем некоторые примеры данных в таблицу. 

  • Шаг 3: Массовая загрузка данных в таблицу

    В этом шаге мы загрузим часть данных в таблицу DynamoDB, созданную ранее. Благодаря этому в дальнейшем у нас будут образцы данных для использования.

    В каталоге scripts/ находится файл items.json. Этот файл содержит 835 примеров случайных элементов, созданных для этого модуля. В их число входят сущностиUser, Game и UserGameMapping. Откройте файл, чтобы просмотреть примеры элементов.

    В каталоге 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 имеют более простой интерфейс для использования API от AWS. Объект 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
    }
    

    Параметр Count должен иметь значение 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/entity.py.

    Реальная работа скрипта заключается в функции fetch_game_and_users, определенной в модуле. Она похожа на функцию, которую вы назначили бы в своем приложении для использования любыми адресами, которым нужны соответствующие данные.

    Функция fetch_game_and_users выполняет несколько вещей.. Сначала она отправляет запрос Query в DynamoDB. Этот запрос Query использует PK атрибута GAME#<GameId>. Затем он запрашивает все сущности, ключ сортировки которых находится в диапазоне между #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.