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

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


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

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

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


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

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

    • Пользователи
    • Фотографии
    • Реакции
    • Дружба

    Эти сущности используют три разных типа отношений между данными.

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

    Кроме того, у пользователя есть несколько фотографий для отображения в приложении, а каждая фотография может иметь несколько реакций. Такие отношения называются «один ко многим».

    И наконец, сущность Дружба представляет отношения «многие ко многим». Сущность Дружба обозначает, что один из пользователей в приложении подписан на другого пользователя. Это отношения «много-ко-многим», ведь каждый пользователь может подписаться на нескольких других пользователей, и на него могут быть подписаны несколько пользователей.

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

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

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

    Учитывая это, рекомендуется использовать следующий шаблон для значений HASH и RANGE каждого типа сущности.

    Сущность

    HASH

    RANGE

    Пользователь

    USER#<USERNAME>

    #METADATA#<USERNAME>

    Фотография

    USER#<USERNAME>

    PHOTO#<USERNAME>#<TIMESTAMP>

    Реакция

    REACTION#<USERNAME>#<TYPE>

    PHOTO#<USERNAME>#<TIMESTAMP>

    Дружба

    USER#<USERNAME>

    #FRIEND#<FRIEND_USERNAME>

    Давайте рассмотрим предыдущую таблицу.

    Для сущности «Пользователь» значение HASH имеет вид USER#<USERNAME>. Обратите внимание, что мы используем префикс для идентификации сущности и предотвращения возможных конфликтов между типами сущностей.

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

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

    Во-вторых, сущность «Фотография» является дочерним элементом для конкретной сущности «Пользователь». Основной шаблон доступа к фотографиям заключается в выборе фотографий для конкретного пользователя с упорядочением по дате. Если вам требуется сортировка по некоторому свойству, это свойство для поддержки такой сортировки необходимо включить в ключ RANGE. Для фотографий используется такой же ключ HASH, как и для пользователей, что позволяет одним запросом извлекать как профиль пользователя, так и все его фотографии. Для ключа RANGE укажите значение PHOTO#<USERNAME>#<TIMESTAMP>, которое будет уникальным образом идентифицировать фотографии в таблице.

    В-третьих, сущность «Реакция» является дочерним элементом для конкретной сущности «Фотография». Между реакциями и фотографиями существует отношение «один ко многим», а значит применима вся та же логика, что и для сущности «Фотография». В следующем модуле вы увидите, как можно одним запросом извлекать фотографию и все реакции для нее, используя вторичный индекс. А пока можно просто отметить, что ключ RANGE для реакций строится по тому же шаблону, что и ключ RANGE для фотографий. Для ключа HASH мы применяем имя пользователя, который создал эту реакцию, в сочетании с типом реакции. Включение типа реакции позволяет пользователю добавлять несколько реакций разных типов к одной фотографии.

    Наконец, сущность «Дружба» использует тот же ключ HASH, что и сущность «Пользователь». Это позволяет одним запросом извлекать и метаданные пользователя, и список его подписчиков. Ключ RANGE для сущности «Дружба» имеет значение #FRIEND#<FRIEND_USERNAME>. В шаге 4 ниже вы узнаете, для чего в ключе RANGE сущности «Дружба» используется префикс «#».

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

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

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

    Код, загруженный в шаге 3 модуля 1, содержит скрипт Python под названием create_table.py в каталоге scripts/. Содержание скрипта Python приведено ниже.

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.create_table(
            TableName='quick-photos',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            KeySchema=[
                {
                    "AttributeName": "PK",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "SK",
                    "KeyType": "RANGE"
                }
            ],
            ProvisionedThroughput={
                "ReadCapacityUnits": 5,
                "WriteCapacityUnits": 5
            }
        )
        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, а первичным ключом реакции – ее TYPE. Соответственно, для атрибутов используются общие имена, например PK (для ключа секции) и SK (для ключа сортировки).

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

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

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

    python scripts/create_table.py

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

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

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

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

    В каталоге scripts/ есть файл items.json. Этот файл содержит 967 примеров случайных элементов, созданных для этого модуля. Эти элементы соответствуют сущностям Пользователь, Фотография, Дружба и Реакция. Откройте файл, чтобы просмотреть примеры элементов.

    В каталоге scripts/ также есть файл bulk_load_table.py, который считывает элементы из файла items.json и записывает их в таблицу DynamoDB. Файл содержит следующий код:

    import json
    
    import boto3
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('quick-photos')
    
    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 полезен для использования в этой ситуации, поскольку он объединяет наши запросы. Операция APIBatchWriteItem принимает до 25 элементов в одном запросе. Объект Resource самостоятельно группирует запросы, благодаря чему нам не нужно следить за тем, чтобы запросы возвращали 25 или меньше элементов.

    Запустите скрипт bulk_load_table.py и загрузите таблицу с данными, выполнив следующую команду в терминале.

    python scripts/bulk_load_table.py

    Чтобы убедиться, что все данные загружены в таблицу, выполните подсчет элементов с помощью операции Scan.

    Выполните следующую операцию, чтобы получить результаты подсчета от AWS CLI:

    aws dynamodb scan \
     --table-name quick-photos \
     --select COUNT

    При этом должны отобразиться следующие результаты.

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

    Параметр Count должен иметь значение 967. Это значит, что все элементы загружены успешно.

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

  • Шаг 4. Получение нескольких типов сущностей в одном запросе

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

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

    Этот запрос охватывает два типа сущностей: Пользователь и Фотография. Однако это не значит, что нам потребуется несколько запросов.

    В скачанном коде в каталоге application/ есть файл под названием fetch_user_and_photos.py. Этот скрипт демонстрирует, как структурировать код для получения сущности Пользователь и всех загруженных им фотографий в одном запросе.

    Код скрипта fetch_user_and_photos.py приведен ниже.

    import boto3
    
    from entities import User, Photo
    
    dynamodb = boto3.client('dynamodb')
    
    USER = "jacksonjason"
    
    
    def fetch_user_and_photos(username):
        resp = dynamodb.query(
            TableName='quick-photos',
            KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :photos",
            ExpressionAttributeValues={
                ":pk": { "S": "USER#{}".format(username) },
                ":metadata": { "S": "#METADATA#{}".format(username) },
                ":photos": { "S": "PHOTO$" },
            },
            ScanIndexForward=True
        )
    
        user = User(resp['Items'][0])
        user.photos = [Photo(item) for item in resp['Items'][1:]]
    
        return user
    
    
    user = fetch_user_and_photos(USER)
    
    print(user)
    for photo in user.photos:
        print(photo)

    В начале скрипта мы импортируем библиотеку Boto 3 и некоторые простые классы для представления объектов в коде приложения. Определения этих сущностей содержатся в файле application/entity.py.

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

    В этой функции первым делом выполняется запрос Query к DynamoDB. В Query определен ключ HASH по выражению USER#<Username>, который позволяет отделить элементы, принадлежащие конкретному пользователю.

    Также в Query содержится выражение условия по ключу RANGE, между сегментами #METADATA#<Username> и PHOTO$. По запросу Query возвращает сущность «Пользователь», поскольку используется ключ сортировки #METADATA#<Username>, а также все фотографии этого пользователя с префиксом ключа сортировки PHOTO#. Ключи сортировки строкового типа сортируются по кодам символов ASCII. В таблицах ASCII знак доллара ($) следует непосредственно за знаком фунта (#), что позволяет получить все сопоставления в сущности Photo .

    Получив ответ, мы собираем элементы данных в объекты, известные нашему приложению. Нам известно, что первым элементом возвращается сущность Пользователь, поэтому из нее мы создадим объект User. Для остальных сущностей мы создаем объекты Photo, а массив пользователей прикрепляем к объекту User.

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

    python application/fetch_user_and_photos.py

    Она выведет в консоль объект User и все объекты Photo:

    User<jacksonjason -- John Perry>
    Photo<jacksonjason -- 2018-05-30T15:42:38>
    Photo<jacksonjason -- 2018-06-09T13:49:13>
    Photo<jacksonjason -- 2018-06-26T03:59:33>
    Photo<jacksonjason -- 2018-07-14T10:21:01>
    Photo<jacksonjason -- 2018-10-06T22:29:39>
    Photo<jacksonjason -- 2018-11-13T08:23:00>
    Photo<jacksonjason -- 2018-11-18T15:37:05>
    Photo<jacksonjason -- 2018-11-26T22:27:44>
    Photo<jacksonjason -- 2019-01-02T05:09:04>
    Photo<jacksonjason -- 2019-01-23T12:43:33>
    Photo<jacksonjason -- 2019-03-03T02:00:01>
    Photo<jacksonjason -- 2019-03-03T18:20:10>
    Photo<jacksonjason -- 2019-03-11T15:18:22>
    Photo<jacksonjason -- 2019-03-30T02:28:42>
    Photo<jacksonjason -- 2019-04-14T21:52:36>

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


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

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

    • Создать профиль пользователя (Запись)
    • Обновление профиля пользователя (Запись)
    • Получение профиля пользователя (Чтение)
    • Загрузить фотографию (Запись)
    • Просмотреть фотографии пользователя (Чтение)
    • Просмотреть подписчиков пользователя (Чтение)

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