ПРИМЕЧАНИЕ ДЛЯ РЕДАКЦИИ: Amazon DynamoDB — это полностью управляемая проприетарная служба баз данных NoSQL, предлагаемая Amazon.com как часть портфеля Amazon Web Services.
DynamoDB предоставляет похожую модель данных и получает свое имя от Dynamo, но имеет другую базовую реализацию. У Dynamo был дизайн с несколькими мастерами, требующий от клиента разрешения конфликтов версий, а DynamoDB использует синхронную репликацию между несколькими центрами обработки данных для обеспечения высокой надежности и доступности.
DynamoDB отличается от других сервисов Amazon тем, что позволяет разработчикам приобретать сервис на основе пропускной способности, а не хранилища. Если автоматическое масштабирование включено, база данных будет масштабироваться автоматически. (Источник: Википедия )
Теперь мы предоставляем исчерпывающее руководство, чтобы вы могли разрабатывать свои собственные приложения на основе Amazon DynamoDB. Мы охватываем широкий спектр тем, от интеграции Java и передового опыта до резервного копирования и ценообразования. С помощью этого руководства вы сможете запустить и запустить собственные проекты за минимальное время. Наслаждайтесь!
1. Введение
Amazon DynamoDB — это сервис базы данных NoSQL, предлагаемый Amazon как часть своего портфеля Amazon Web Service (AWS). Он поддерживает хранение, запрос и обновление документов и предлагает SDK для разных языков программирования.
DynamoDB поддерживает модель данных ключ-значение. Каждый элемент (строка) в таблице является парой ключ-значение. Первичный ключ является единственным обязательным атрибутом для элемента и уникальным образом идентифицирует каждый элемент. Кроме того, DynamoDB не содержит схем, то есть каждый элемент может иметь любое количество атрибутов, а типы атрибутов могут варьироваться от элемента к элементу. Нельзя только запросить первичный ключ, но можно настроить вторичные индексы (глобальные и локальные) для запроса других атрибутов.
Эти вторичные индексы могут быть созданы в любое время, что означает, что вам не нужно определять их при создании таблицы.
Поскольку DynamoDB использует инфраструктуру AWS, она может автоматически масштабироваться и, следовательно, обрабатывать более высокие пропускные способности и увеличивать доступное хранилище во время выполнения. Инфраструктура AWS также обеспечивает высокую доступность и репликацию данных.
между различными регионами, чтобы обслуживать данные более локально.
2. Концепции
2.1 Таблицы, элементы и атрибуты
Таблицы используются для хранения данных и представляют коллекцию предметов. На следующем рисунке изображена таблица с двумя предметами. Каждый элемент имеет первичный ключ (здесь: GeekId) и атрибут с именем «Имя».
Элементы похожи на строки в других системах баз данных, но в DynamoDB нет ограничений на количество элементов, которые могут храниться в одной таблице. Атрибуты похожи на столбцы в реляционных базах данных, но, поскольку DynamoDB не содержит схем,
он не ограничивает тип данных каждого атрибута и не заставляет заранее определять все возможные атрибуты. Помимо приведенного выше примера, атрибуты также могут быть вложенными, то есть, например, адрес атрибута может иметь податрибуты с именем
Улица или Город. Поддерживается до 32 уровней вложенных атрибутов.
2.2 Первичные ключи
Первичный ключ уникально идентифицирует каждый элемент таблицы. DynamoDB поддерживает два разных вида первичных ключей:
- Ключ раздела / атрибут Hash
- Ключ раздела и ключ сортировки / Хэш и атрибут диапазона
Первый тип является скалярным значением, то есть атрибутом, который может содержать только одно значение строки типа, числа и двоичного файла. DynamoDB вычисляет хэш значения атрибута, чтобы определить раздел, в котором будет храниться соответствующий элемент. Таблица разделена на различные разделы, каждый раздел хранит подмножество всех значений. Таким образом, DynamoDB может работать одновременно на разных разделах и делить большие задачи на более мелкие.
Второй тип первичного ключа состоит из двух значений: ключа раздела и ключа сортировки. Следовательно, это своего рода составной ключ. Как и в первом случае, ключ разделения используется для определения раздела, в котором хранится текущий элемент. В отличие от таблицы, содержащей только ключ раздела, элементы в таблице с дополнительным ключом сортировки сохраняются в порядке сортировки Ключ сортировки. Этот способ хранения данных обеспечивает большую гибкость при запросе данных, поскольку запросы диапазона могут выполняться более эффективно. Если вы, например, сохраните проекты сотрудника в одной таблице и определите проект в качестве ключа сортировки, DynamoDB сможет обслуживать запросы, которые должны легче доставлять подмножество проектов сотрудника. В таких таблицах могут существовать два элемента с одинаковым ключом раздела, но они должны иметь разные ключи сортировки. Кортеж (ключ раздела / ключ сортировки) должен быть уникальным для каждого элемента.
Поскольку раздел элемента вычисляется путем применения хэш-функции к ключу раздела, ключ раздела часто называют «атрибутом хеша». Кроме того, поскольку ключ сортировки облегчает запросы диапазона, его часто называют «атрибутом диапазона».
2.3 Вторичные индексы
Часто требуется запрашивать данные не только по первичному ключу, но также и по другим атрибутам. Для ускорения таких запросов можно создавать индексы, которые содержат подмножество атрибутов базовой таблицы. Эти индексы могут быть запрошены намного быстрее.
DynamoDB различает два разных вида вторичных индексов:
- Глобальный вторичный индекс
- Локальный вторичный индекс
Глобальный вторичный индекс имеет ключ разделения и ключ сортировки, которые могут отличаться от ключа базовой таблицы. Он называется «глобальным» индексом, поскольку запросы по этому индексу могут охватывать все данные в базовой таблице.
В отличие от глобальных вторичных индексов, локальный вторичный индекс имеет тот же ключ секционирования, что и базовая таблица, но другой ключ сортировки. Поскольку базовая таблица и индекс имеют один и тот же ключ раздела, все разделы базовой таблицы имеют соответствующий раздел в индексе. Следовательно, разделы индекса являются «локальными» по отношению к разделам базовой таблицы.
При принятии решения, должен ли индекс быть глобальным или локальным, необходимо учитывать несколько аспектов. В то время как локальные индексы могут иметь только составные ключи, которые состоят из первичного ключа и ключа сортировки, глобальный индекс также может иметь первичный ключ, который состоит только из ключа раздела. Ключ раздела глобального индекса может отличаться от ключа локального индекса. К сожалению, размер всех локальных индексов ограничен (в настоящее время 10 ГБ), тогда как глобальные индексы не имеют никаких ограничений по размеру.
Еще одним недостатком локальных индексов является то, что вы можете создавать их только при создании его базовой таблицы. Создание локального индекса, когда базовая таблица уже создана, не поддерживается. Это отличается от глобальных индексов, которые можно создавать и удалять в любое время. С другой стороны, локальные индексы имеют то преимущество, что они обеспечивают строгую согласованность, в то время как глобальные индексы поддерживают только возможную согласованность. Кроме того, локальные индексы позволяют извлекать данные из базовой таблицы во время запросов, что не поддерживается для глобальных индексов.
Количество индексов в таблице ограничено пятью для глобальных и локальных индексов. При удалении таблицы все ее индексы также удаляются.
2.4 Потоки
Интересной особенностью DynamoDB являются потоки. DynamoDB создает новую запись и вставляет ее в поток в случае, если происходит одно из следующих событий:
- Новый элемент добавляется в таблицу.
- Элемент в таблице обновлен.
- Элемент удален из таблицы.
Запись содержит рядом с метаданными о событии, например, его метку времени, а также копию элемента в момент события. В случае обновлений он даже содержит изображение до и после обновления. Если поток включен для таблицы, его можно использовать для реализации триггеров. Таким образом, вы можете выполнить действие, если произойдет одно из указанных выше событий. Можно, например, отправить электронное письмо, если в таблице хранится новый заказ, или заказать новые статьи, если его нет в наличии. Эта функция также может быть использована для реализации репликации данных или того, что известно в другой системе базы данных как материализованные представления.
Записи в потоке удаляются автоматически через 24 часа.
3. Java SDK
3.1 Настройка
В этом уроке мы будем использовать maven в качестве системы сборки. Следовательно, первым шагом является создание локального проекта Maven. Это можно сделать, вызвав следующую команду в командной строке:
1
|
mvn archetype:generate -DgroupId=com.javacodegeeks.aws -DartifactId=dynamodb -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode= false |
Это создает простой проект maven со следующей разметкой в файловой системе:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
|-- pom.xml `-- src |-- main | `-- java | `-- com | `-- javacodegeeks | `-- aws | `-- App.java `-- test `-- java `-- com `-- javacodegeeks `-- aws `-- AppTest.java |
Сначала мы редактируем файл pom.xml
и добавляем следующую зависимость:
1
2
3
4
5
|
< dependency > < groupId >com.amazonaws</ groupId > < artifactId >DynamoDBLocal</ artifactId > < version >[1.11,2.0)</ version > </ dependency > |
Это говорит maven загрузить локальную версию DynamoDB (и все ее зависимости) в наш локальный репозиторий maven, чтобы мы могли создать локальный работающий экземпляр базы данных. Эта локальная база данных имеет то преимущество, что мы можем отправлять столько запросов, сколько хотим, без необходимости платить за нее. После того, как наше приложение было разработано и протестировано, мы можем изменить его для использования экземпляра DynamoDB, работающего в инфраструктуре AWS.
Поскольку приведенная выше зависимость недоступна в Central Maven Repository, мы должны добавить Amazon репозиторий в файл pom.xml
:
1
2
3
4
5
6
7
|
< repositories > < repository > < id >dynamodb-local-oregon</ id > < name >DynamoDB Local Release Repository</ name > </ repository > </ repositories > |
В приведенном выше примере используется хранилище, расположенное во Франкфурте, Германия. Если вы предпочитаете другое место, которое ближе к вам, вы можете выбрать один из списка здесь . На этой странице также объясняется, как загрузить дистрибутив DynamoDB, который можно запустить локально из командной строки, если вы предпочитаете это. В этом руководстве мы используем зависимость maven, поскольку она позволяет нам запускать экземпляр в памяти непосредственно из нашего кода.
Поскольку экземпляр локальной базы данных использует собственные библиотеки, он должен быть извлечен в локальном каталоге. Мы используем плагин зависимостей maven для извлечения необходимых зависимостей в целевой каталог сборки:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
< build > < plugins > < plugin > < groupId >org.apache.maven.plugins</ groupId > < artifactId >maven-dependency-plugin</ artifactId > < version >3.0.1</ version > < executions > < execution > < id >copy-dependencies</ id > < phase >process-test-resources</ phase > < goals > < goal >copy-dependencies</ goal > </ goals > < configuration > < outputDirectory >${project.build.directory}/dependencies</ outputDirectory > < overWriteReleases >false</ overWriteReleases > < overWriteSnapshots >false</ overWriteSnapshots > < overWriteIfNewer >true</ overWriteIfNewer > </ configuration > </ execution > </ executions > </ plugin > </ plugins > </ build > |
Чтобы запустить наш пример приложения в сборке maven, следующий профиль использует плагин maven exec для запуска задачи:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
< profiles > < profile > < id >exec</ id > < build > < plugins > < plugin > < groupId >org.codehaus.mojo</ groupId > < artifactId >exec-maven-plugin</ artifactId > < version >1.6.0</ version > < executions > < execution > < phase >package</ phase > < configuration > < executable >java</ executable > < arguments > < argument >-cp</ argument > < classpath /> < argument >-Dsqlite4java.library.path=${basedir}/target/dependencies</ argument > < argument >com.javacodegeeks.aws.App</ argument > < argument >${app.task}</ argument > </ arguments > </ configuration > < goals > < goal >exec</ goal > </ goals > </ execution > </ executions > </ plugin > </ plugins > </ build > </ profile > </ profiles > |
Приложение, которое мы разработаем, использует в качестве первого аргумента имя «задачи». В нашем фрагменте выше мы определяем эту задачу с помощью свойства maven. Это свойство может быть установлено в командной строке вместе с активацией профиля:
1
|
mvn package -Pexec -Dapp.task=ListTables |
Чтобы запустить локальный сервер из кода Java, необходимо несколько строк кода:
1
2
3
4
5
6
|
DynamoDBProxyServer server = ServerRunner .createServerFromCommandLineArgs( new String[]{ "-dbPath" , System.getProperty( "user.dir" ) + File.separator + "target" , "-port" , port }); server.start(); |
Класс ServerRunner
имеет статический метод createServerFromCommandLineArgs()
который принимает ряд аргументов. В нашем примере мы определяем, что сервер должен использовать указанный каталог для хранения своих файлов и прослушивания на указанном порту.
После запуска сервера мы можем подключиться с помощью AmazonDynamoDBClientBuilder
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
AmazonDynamoDB dynamodb = AmazonDynamoDBClientBuilder .standard() .withEndpointConfiguration( ) .withCredentials( new AWSStaticCredentialsProvider( new AWSCredentials() { @Override public String getAWSAccessKeyId() { return "dummy" ; } @Override public String getAWSSecretKey() { return "dummy" ; } })) .build(); |
Метод builder standard()
создает экземпляр базы данных со значениями конфигурации по умолчанию. Конечная точка настраивается с использованием экземпляра AwsClientBuilder.EndpointConfiguration
и передачей URL-адреса и региона Amazon в качестве аргументов. Поскольку мы запустили сервер на localhost
хосте, мы настроили конечную точку на том же хосте. В реальных приложениях вы должны использовать IP-адрес или имя хоста, на котором работает экземпляр DynamoDB.
Локальная база данных игнорирует регион Амазонки, поэтому мы можем предоставить статическую строку us-west-2
. То же самое верно для ключа доступа и секретного ключа. Фиктивные значения, как показано выше, достаточны для подключения. В любом случае, в реальных сценариях вы не предоставите их жестко закодированным, а используете другого поставщика. SDK поставляется с поставщиками, которые считывают значения из переменных среды, системных свойств, файлов профилей и т. Д. При необходимости можно бесплатно реализовать определенного поставщика. Более подробную информацию по этой теме можно найти здесь .
Наконец, вызов build()
создает новый экземпляр AmazonDynamoDB
, клиентский объект, который мы будем использовать для взаимодействия с базой данных.
Теперь, когда мы знаем, как запустить локальную базу данных и как подключиться, мы можем окружить приведенные выше фрагменты кода операторами, которые запускают код класса Java, просматривая его имя по отражению и передавая ему AmazonDynamoDB
экземпляр AmazonDynamoDB
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
public static void main(String[] args) throws Exception { if (args.length < 1 ) { System.err.println( "Please provide the task to run." ); System.exit(- 1 ); } String taskName = args[ 0 ]; try { Class<?> aClass = Class.forName( "com.javacodegeeks.aws.tasks." + taskName); Object newInstance = aClass.newInstance(); if (newInstance instanceof Task) { Task task = (Task) newInstance; String[] argsCopy = new String[args.length - 1 ]; System.arraycopy(args, 1 , argsCopy, 0 , args.length - 1 ); AmazonDynamoDB dynamodb = null ; String port = "8000" ; DynamoDBProxyServer server = null ; try { server = ServerRunner.createServerFromCommandLineArgs( new String[]{ "-inMemory" , "-port" , port}); server.start(); dynamodb = AmazonDynamoDBClientBuilder.standard() .withEndpointConfiguration( new AwsClientBuilder.EndpointConfiguration( "http://localhost:" + port, "us-west-2" )) .withCredentials( new AWSStaticCredentialsProvider( new AWSCredentials() { @Override public String getAWSAccessKeyId() { return "dummy" ; } @Override public String getAWSSecretKey() { return "dummy" ; } })) .build(); task.run(argsCopy, dynamodb); } finally { if (server != null ) { server.stop(); } if (dynamodb != null ) { dynamodb.shutdown(); } } } else { System.err.println( "Class " + aClass.getName() + " does not implement interface " + Task. class .getName() + "." ); } } catch (ClassNotFoundException e) { System.err.println( "No task with name '" + taskName + " available." ); } catch (InstantiationException | IllegalAccessException e) { System.err.println( "Cannot create instance of task '" + taskName + ": " + e.getLocalizedMessage()); } } |
Это позволяет нам легко добавлять больше задач в приложение без повторения одного и того же кода. Мы также передаем задаче дополнительные аргументы, предоставленные в командной строке, чтобы мы могли предоставить ей дополнительную информацию.
Если вы не хотите жестко кодировать имя хоста и порт, вы можете легко настроить приведенный выше пример кода в соответствии со своими потребностями.
3.2 Таблицы
В этом разделе мы увидим, как работать с таблицами. Самая простая операция — перечислить все доступные таблицы. Это можно сделать с помощью следующей «задачи»:
01
02
03
04
05
06
07
08
09
10
11
|
public class ListTables implements Task { @Override public void run(String[] args, AmazonDynamoDB dynamodb) { ListTablesResult listTablesResult = dynamodb.listTables(); List<String> tableNames = listTablesResult.getTableNames(); System.out.println( "Number of tables: " + tableNames.size()); for (String tableName : tableNames) { System.out.println( "Table: " + tableName); } } } |
Как мы видим, в AmazonDynamoDB
есть метод listTables()
который возвращает объект ListTablesResult
. Этот объект раскрывает список таблиц, вызывая его метод getTableNames()
. com.javacodegeeks.aws.tasks
этот класс в пакет com.javacodegeeks.aws.tasks
мы можем вызвать его как часть сборки maven:
1
|
mvn package -Pexec -Dapp.task=ListTables |
Это выведет что-то вроде:
01
02
03
04
05
06
07
08
09
10
|
[INFO] --- exec -maven-plugin:1.6.0: exec (default) @ dynamodb --- Initializing DynamoDB Local with the following configuration: Port: 8000 InMemory: true DbPath: null SharedDb: false shouldDelayTransientStatuses: false CorsParams: * Number of tables: 0 |
Как и ожидалось, в нашем экземпляре еще нет таблиц. Однако мы можем создать первую таблицу, используя следующий код:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class CreateProductTable implements Task { @Override public void run(String[] args, AmazonDynamoDB dynamodb) { ArrayList<AttributeDefinition> attributeDefinitions = new ArrayList<>(); attributeDefinitions.add( new AttributeDefinition() .withAttributeName( "ID" ).withAttributeType( "S" )); ArrayList<KeySchemaElement> keySchema = new ArrayList<>(); keySchema.add( new KeySchemaElement() .withAttributeName( "ID" ).withKeyType(KeyType.HASH)); CreateTableRequest createTableReq = new CreateTableRequest() .withTableName( "Product" ) .withAttributeDefinitions(attributeDefinitions) .withKeySchema(keySchema) .withProvisionedThroughput( new ProvisionedThroughput() .withReadCapacityUnits(10L) .withWriteCapacityUnits(5L)); CreateTableResult result = dynamodb.createTable(createTableReq); System.out.println(result.toString()); } } |
Сначала мы определяем атрибуты таблицы, но мы определяем только атрибуты, которые являются частью ключа. В этом простом случае мы создадим таблицу для управления продуктами нашей компании; следовательно, мы назовем таблицу «Product» и будем использовать искусственный первичный ключ с именем ID
. Этот ключ имеет тип string, поэтому мы выбрали S
качестве типа атрибута. Список KeySchemaElement
заполнен одним элементом, который определяет атрибут ID как хеш-ключ.
При создании новой таблицы всегда необходимо указывать начальную емкость для чтения и записи. Amazon можно автоматически масштабировать экземпляр при работе в облаке AWS, но даже в этом случае вы должны указать эти две скорости при создании таблицы. Также возможно настроить эти параметры позже, когда ваши приложения масштабируются. Пропускная способность означает число строго согласованных чтений в секунду или два в конечном итоге согласованных чтений в секунду. В нашем примере кода выше мы указываем, что нам нужно 10 строго согласованных чтений в секунду (или 20 в конечном итоге согласованных чтений).
Разница между этими типами чтения заключается в том, что строго согласованные чтения гарантируют, что последние записанные данные будут возвращены, в то время как возможная согласованность означает, что чтение может вернуть устаревшие данные. Повторение того же возможного непротиворечивого чтения через несколько секунд может вернуть значения, которые являются более актуальными. Поскольку AWS распределяет разделы таблицы по облаку AWS, использование строгого согласованного чтения требует, чтобы все предыдущие операции записи были включены в результат. Такой тип чтения не возвращает действительный результат в случае сетевых задержек или сбоев.
С другой стороны, емкость записи определяет количество записей в 1 КБ в секунду. Пакеты данных размером менее 1 КБ округляются, поэтому запись только 500 байтов считается за 1 КБ записи. Наш пример кода выше говорит DynamoDB масштабировать систему так, чтобы мы могли записывать до 5 раз в секунду около 1 КБ данных.
Возвращаемое значение вызова createTable()
содержит описание таблицы:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
{ TableDescription: { AttributeDefinitions: [{ AttributeName: ID, AttributeType: S }], TableName: Product, KeySchema: [{ AttributeName: ID, KeyType: HASH }], TableStatus: ACTIVE, CreationDateTime: Sun Sep 10 13:41:10 CEST 2017, ProvisionedThroughput: { LastIncreaseDateTime: ThuJan0101: 00: 00CET1970, LastDecreaseDateTime: ThuJan0101: 00: 00CET1970, NumberOfDecreasesToday: 0, ReadCapacityUnits: 10, WriteCapacityUnits: 5 }, TableSizeBytes: 0, ItemCount: 0, TableArn: arn: aws: dynamodb: ddblocal: 000000000000: table/Product, } } |
Вывод отражает сделанные нами изменения: ID
атрибута определяется как строка и ключ хеша, таблица уже активна, и мы можем видеть определенные скорости чтения и записи. Мы также получаем информацию о количестве элементов, хранящихся в таблице, и размере таблицы.
3.3 Предметы
Теперь, когда мы в таблице, мы можем вставить некоторые образцы данных. Следующая «задача» вставляет два продукта в таблицу Product
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class InsertProducts implements Task { @Override public void run(String[] args, AmazonDynamoDB dynamodb) { DynamoDB dynamoDB = new DynamoDB(dynamodb); Table table = dynamoDB.getTable( "Product" ); String uuid1 = UUID.randomUUID().toString(); Item item = new Item() .withPrimaryKey( "ID" , uuid1) .withString( "Name" , "Apple iPhone 7" ) .withNumber( "Price" , 664.9 ); table.putItem(item); String uuid2 = UUID.randomUUID().toString(); item = new Item() .withPrimaryKey( "ID" , uuid2) .withString( "Name" , "Samsung Galaxy S8" ) .withNumber( "Price" , 543.0 ); table.putItem(item); item = table.getItem( "ID" , uuid1); System.out.println( "Retrieved item with UUID " + uuid1 + ": name=" + item.getString( "Name" ) + "; price=" + item.getString( "Price" )); item = table.getItem( "ID" , uuid2); System.out.println( "Retrieved item with UUID " + uuid2 + ": name=" + item.getString( "Name" ) + "; price=" + item.getString( "Price" )); table.deleteItem( new PrimaryKey( "ID" , uuid1)); table.deleteItem( new PrimaryKey( "ID" , uuid2)); System.out.println(table.describe()); } } |
Сначала мы создаем экземпляр DynamoDB
. Этот класс является точкой входа в «Document API» и предоставляет методы для составления списка таблиц для записи элементов. Здесь мы используем его метод getTable()
для получения ссылки на таблицу DynamoDB. Этот табличный объект имеет метод putItem()
для хранения элемента под указанным первичным ключом. В этом простом примере мы собираемся использовать UUID в качестве идентификатора продукта и предоставить его методу withPrimaryKey()
элемента. Вспомогательные методы, такие как withString()
или withNumber()
позволяют нам предоставлять дополнительные атрибуты. Обратите внимание, что эти атрибуты не нужно указывать при создании таблицы. Это позволяет обрабатывать таблицу без схемы.
После помещения двух элементов в таблицу мы получаем их обратно, запрашивая базу данных для конкретного первичного ключа. Мы также выводим два дополнительных атрибута, которые мы вставили ранее. Это произведет вывод, подобный следующему:
1
2
|
Retrieved item with UUID 7dc5be93-213b-4b16-95f9-5ee7381b219d: name=Apple iPhone 7; price=664.9 Retrieved item with UUID 06fbf030-93f0-48ca-805f-2e627d72faf4: name=Samsung Galaxy S8; price=543 |
Наконец, мы удаляем два элемента с помощью метода deleteItem()
и передаем первичный ключ, который необходимо удалить. Чтобы убедиться, что все работает как положено, мы можем проверить выходные данные метода describe()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
{ TableDescription: { AttributeDefinitions: [{ AttributeName: ID, AttributeType: S }], TableName: Product, KeySchema: [{ AttributeName: ID, KeyType: HASH }], TableStatus: ACTIVE, CreationDateTime: Sun Sep 10 13:41:10 CEST 2017, ProvisionedThroughput: { LastIncreaseDateTime: Thu Jan 01 01:00:00 CET 1970, LastDecreaseDateTime: Thu Jan 01 01:00:00 CET 1970, NumberOfDecreasesToday: 0, ReadCapacityUnits: 10, WriteCapacityUnits: 5 }, TableSizeBytes: 0, ItemCount: 0, TableArn: arn: aws: dynamodb: ddblocal: 000000000000: table/Product, } } |
Мы уже предполагали, что размер таблицы равен нулю, и что в ней нет элементов. Кроме того, мы не находим ничего о двух дополнительных атрибутах, которые мы вставили ранее. Но это очевидно, потому что в базе данных без схемы, такой как DynamoDB, структура таблицы не знает обо всех доступных атрибутах во время выполнения. Он только управляет основными данными о первичном ключе, используемом для уникальной идентификации каждого элемента.
До сих пор мы видели, как создавать, читать и удалять данные. Чтобы реализовать полнофункциональное приложение, нам все еще нужно обновить данные. Следующий код показывает, как это сделать:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
UpdateItemSpec updateSpec = new UpdateItemSpec() .withPrimaryKey( new PrimaryKey( "ID" , uuid1)) .withUpdateExpression( "set Price = :priceNew" ) .withConditionExpression( "Price = :priceOld" ) .withValueMap( new ValueMap() .withNumber( ":priceOld" , 664.9 ) .withNumber( ":priceNew" , 645.9 ) ); table.updateItem(updateSpec); item = table.getItem( "ID" , uuid1); System.out.println( "Retrieved item with UUID " + uuid1 + ": name=" + item.getString( "Name" ) + "; price=" + item.getString( "Price" )); |
Если поместить в класс InsertProducts
выше непосредственно после получения элементов, он обновит цену первого элемента в случае, если он имеет определенное значение. Поэтому мы определяем set Price = :priceNew
выражений set Price = :priceNew
как выражение обновления, а выражение Price = :priceOld
как условие. Значения переменных :priceNew
и :priceOld
определяются с помощью ValueMap
. Передача UpdateItemSpec
вызову updateItem()
позволяет DynamoDB обновить цену элемента до 645,9, если его текущая цена равна 664,9. Следующее извлечение элемента выводит это на консоль, чтобы убедиться, что все работает должным образом.
3.4 Пакетные операции
Часто, особенно при миграции существующих систем, большое количество запросов должно быть обработано за короткий промежуток времени. Вместо того, чтобы выдавать все эти запросы как отдельные, пакетные операции позволяют собирать определенное количество операций и отправлять их одним запросом в базу данных. Это уменьшает накладные расходы на отправку запроса и ожидание ответа. Для больших наборов операций это может значительно повысить производительность приложений.
Следующий код демонстрирует, как пакетно вставить два продукта, которые мы вставили раньше, один за другим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
public class BatchInsertProducts implements Task { @Override public void run(String[] args, AmazonDynamoDB dynamodb) { DynamoDB dynamoDB = new DynamoDB(dynamodb); String uuid1 = UUID.randomUUID().toString(); String uuid2 = UUID.randomUUID().toString(); TableWriteItems tableWriteItems = new TableWriteItems( "Product" ) .withItemsToPut( new Item() .withPrimaryKey( "ID" , uuid1) .withString( "Name" , "Apple iPhone 7" ) .withNumber( "Price" , 664.9 ), new Item() .withPrimaryKey( "ID" , uuid2) .withString( "Name" , "Samsung Galaxy S8" ) .withNumber( "Price" , 543.0 )); BatchWriteItemSpec spec = new BatchWriteItemSpec() .withProgressListener( new ProgressListener() { @Override public void progressChanged(ProgressEvent progressEvent) { System.out.println(progressEvent.toString()); } }) .withTableWriteItems(tableWriteItems); BatchWriteItemOutcome outcome = dynamoDB.batchWriteItem(spec); System.out.println( "Unprocessed items: " + outcome.getUnprocessedItems().size()); TableKeysAndAttributes tableKeyAndAttributes = new TableKeysAndAttributes( "Product" ) .withPrimaryKeys( new PrimaryKey( "ID" , uuid1), new PrimaryKey( "ID" , uuid2)); BatchGetItemSpec getSpec = new BatchGetItemSpec() .withProgressListener( new ProgressListener() { @Override public void progressChanged(ProgressEvent progressEvent) { System.out.println(progressEvent.toString()); } }) .withTableKeyAndAttributes(tableKeyAndAttributes); BatchGetItemOutcome batchGetItemOutcome = dynamoDB.batchGetItem(getSpec); System.out.println( "Unprocessed keys: " + batchGetItemOutcome.getUnprocessedKeys().size()); BatchGetItemResult batchGetItemResult = batchGetItemOutcome.getBatchGetItemResult(); Map<String, List<Map<String, AttributeValue>>> responses = batchGetItemResult.getResponses(); for (Map.Entry<String, List<Map<String, AttributeValue>>> entry : responses.entrySet()) { String tableName = entry.getKey(); System.out.println( "Table: " + tableName); List<Map<String, AttributeValue>> items = entry.getValue(); for (Map<String, AttributeValue> item : items) { for (Map.Entry<String, AttributeValue> itemEntry : item.entrySet()) { System.out.println( "\t " + itemEntry.getKey() + "=" + itemEntry.getValue()); } } } Table table = dynamoDB.getTable( "Product" ); table.deleteItem( new PrimaryKey( "ID" , uuid1)); table.deleteItem( new PrimaryKey( "ID" , uuid2)); } } |
Опять же, мы используем «API документа» SDK, создавая экземпляр DynamoDB
. Это позволяет нам вызывать его метод batchWriteItem()
с экземпляром BatchWriteItemSpec
качестве аргумента. Эта спецификация пакетного запроса принимает, конечно, список элементов (здесь представленных как TableWriteItems
), но дополнительно также прослушиватель прогресса. Таким образом, наше приложение получает информацию об обновлениях пакетного процесса. Это полезно при загрузке огромных объемов данных в базу данных.
Экземпляр TableWriteItems
получает имя таблицы в качестве аргумента для конструктора и список Item
качестве параметров его метода с withItemsToPut()
. Два небольших элемента, конечно, не являются репрезентативными для пакетного процесса, но этого должно быть достаточно для демонстрации работы API. Обратите внимание, что вы должны соответствовать емкости записи, определенной при создании таблицы. Следовательно, может случиться, что пакетный процесс запускает процесс регулирования на стороне AWS при быстрой записи в базу данных. В нашем случае этого не произойдет, потому что мы запросили до 10 операций записи в секунду, но при работе с большими наборами данных вы можете столкнуться с этим. В этом случае необходимо проверить количество необработанных предметов. Возвращается как возвращаемое значение метода batchWriteItem()
. В случае, если есть необработанные элементы, нужно попробовать еще раз, чтобы вставить их.
Пакетный API также позволяет получать большие наборы элементов. Опять же, мы должны убедиться, что мы не нарушаем указанную емкость чтения таблицы. Однако в нашем небольшом примере этого не должно произойти. Извлечение больших наборов элементов можно выполнить, вызвав метод batchGetItem()
с экземпляром BatchGetItemSpec
. Как и в случае записи, мы можем передать ProgressListener
. Кроме того, мы также должны передать список предметов для извлечения. В этом случае мы предоставляем их в виде списка объектов PrimaryKey
. Имя таблицы может быть передано в конструктор TableKeysAndAttributes
. Таким образом, можно даже указать списки элементов для разных таблиц.
В результате вызова batchGetItem()
API возвращает экземпляр BatchGetItemOutcome
. Количество необработанных ключей является индикатором, если мы должны повторить некоторые операции. Результат также содержит список пунктов, которые мы запросили. Форма этого списка немного некрасива, так как состоит из карты пар ключ / значение внутри списка. Кроме того, у нас есть список для каждой таблицы, то есть список снова находится внутри карты. Поэтому мы должны перебрать карту с именами таблиц, чтобы сначала получить список элементов для каждой таблицы. Этот список снова является картой для каждого элемента с парами ключ / значение.
Наконец, мы удаляем два элемента, которые мы создали, используя пакетный API.
3.5 DB Mapper
Последний пример показал, насколько утомительной может быть работа с картами и списками атрибутов. Для облегчения работы Amazon также предоставляет инфраструктуру сопоставления, которая позволяет отображать обычные классы Java на таблицы в DynamoDB.
Как и в других средах отображения, простые старые объекты Java (POJO) используются для передачи данных в базу данных. Аннотации говорят картографической структуре, как хранить данные. Следующий класс показывает такой простой аннотированный POJO:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@DynamoDBTable (tableName= "Product" ) public class Product { private String id; private String name; private double price; @DynamoDBHashKey (attributeName = "ID" ) @DynamoDBAutoGeneratedKey public String getId() { return id; } public void setId(String id) { this .id = id; } @DynamoDBAttribute (attributeName= "Name" ) public String getName() { return name; } public void setName(String name) { this .name = name; } @DynamoDBAttribute (attributeName= "Price" ) public double getPrice() { return price; } public void setPrice( double price) { this .price = price; } } |
Аннотация @DynamoDBTable
используется на уровне класса и сообщает платформе, что экземпляры этого класса хранятся в таблице Product
. Product
следует бобовым соглашениям, согласно которым все его атрибуты доступны через методы получения и установки. Аннотации к методам получения показывают, из каких атрибутов состоит продукт. Аннотация @DynamoDBHashKey
указывает, что id
атрибута должен содержать хеш-ключ объекта. Поскольку для идентификатора мы использовали другое имя в нижнем и верхнем регистре, мы должны указать это с помощью атрибута attributeName
аннотации. Таким образом, маппер знает, как сохранить хеш-ключ как ID
атрибута. В предыдущих примерах мы использовали случайный UUID в качестве хеш-ключа. Вместо того, чтобы писать код для создания UUID самостоятельно, мы можем позволить мапперу создать случайный UUID. Это достигается путем аннотирования метода-получателя хеш-ключа с помощью @DynamoDBAutoGeneratedKey
.
Аннотация @DynamoDBAttribute
помещается в остальные методы получения объекта Product
и сообщает мапперу, что продукт имеет атрибут name и атрибут price.
Реализовав этот простой класс POJO, мы можем использовать его для сохранения тех же продуктов, что и в примере с классом Item
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public class MapperInsertProducts implements Task { @Override public void run(String[] args, AmazonDynamoDB dynamodb) { DynamoDBMapper mapper = new DynamoDBMapper(dynamodb); Product product1 = new Product(); product1.setName( "Apple iPhone 7" ); product1.setPrice( 664.9 ); mapper.save(product1); System.out.println( "Saved product with ID " + product1.getId()); Product product2 = new Product(); product2.setName( "Samsung Galaxy S8" ); product2.setPrice( 543.0 ); mapper.save(product2); System.out.println( "Saved product with ID " + product2.getId()); Product product = mapper.load(Product. class , product1.getId()); System.out.println( "Loaded product: id=" + product.getId() + "; name=" + product.getName() + "; price=" + product.getPrice()); product = mapper.load(Product. class , product2.getId()); System.out.println( "Loaded product: id=" + product.getId() + "; name=" + product.getName() + "; price=" + product.getPrice()); mapper.delete(product1); mapper.delete(product2); } } |
На первом этапе мы создаем экземпляр DynamoDBMapper
. Этот класс предоставляет все методы, которые нам нужны для взаимодействия со структурой отображения. Вторым шагом является создание экземпляра Product
и заполнение его полей образцами данных.
Хранение этих атрибутов в базе данных выполняется простым вызовом метода save()
маппера. Он также обновляет хеш-ключ элемента, как мы уже говорили картографической структуре для его автоматической генерации.
Чтобы проверить, что данные фактически были вставлены в базу данных, мы вызываем метод load()
картографа. Первый аргумент сообщает ему, какой базовый тип мы хотим заполнить значениями из базы данных. Помимо этого картограф также должен знать идентификатор элемента для загрузки. С помощью этой информации он может загрузить данные по идентификатору и заполнить новый экземпляр класса Product
значениями. Вывод этих значений в консоль позволяет нам убедиться, что мы на самом деле получили те же значения, что и ранее.
Наконец, мы используем метод delete()
чтобы удалить два новых продукта из таблицы.
3.6 Запросы
Поскольку база данных используется не только для хранения, но и для запроса данных, на этом этапе мы должны рассмотреть, как создавать запросы к DynamoDB. Поэтому нам нужна таблица с некоторыми примерами данных, которые мы можем запросить. Эта таблица создается на первом этапе:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
private void createTodoListTable(AmazonDynamoDB dynamodb) { ListTablesResult listTablesResult = dynamodb.listTables(); List<String> tableNames = listTablesResult.getTableNames(); for (String tableName : tableNames) { if ( "TodoList" .equals(tableName)) { DeleteTableRequest deleteTableReq = new DeleteTableRequest().withTableName(tableName); dynamodb.deleteTable(deleteTableReq); System.out.println( "Deleted table " + tableName); } } ArrayList<AttributeDefinition> attributeDefinitions = new ArrayList<>(); attributeDefinitions.add( new AttributeDefinition() .withAttributeName( "Category" ).withAttributeType( "S" )); attributeDefinitions.add( new AttributeDefinition() .withAttributeName( "CreationDate" ).withAttributeType( "S" )); ArrayList<KeySchemaElement> keySchema = new ArrayList<>(); keySchema.add( new KeySchemaElement() .withAttributeName( "Category" ).withKeyType(KeyType.HASH)); keySchema.add( new KeySchemaElement() .withAttributeName( "CreationDate" ).withKeyType(KeyType.RANGE)); CreateTableRequest createTableReq = new CreateTableRequest() .withTableName( "TodoList" ) .withAttributeDefinitions(attributeDefinitions) .withKeySchema(keySchema) .withProvisionedThroughput( new ProvisionedThroughput() .withReadCapacityUnits(10L) .withWriteCapacityUnits(5L)); CreateTableResult result = dynamodb.createTable(createTableReq); System.out.println( "Created table " + result.toString()); } |
Если таблица уже существует, мы удаляем ее. Затем мы определяем два атрибута типа string («S»): Category и CreationDate. Значения даты хранятся в DynamoDB как строки ISO-8601. Категория действует как ключ хеша, а дата создания — как ключ диапазона.
Простой класс POJO сопоставляет поля класса с атрибутами базы данных:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
@DynamoDBTable (tableName= "TodoList" ) public class TodoItem { private String category; private Date creationDate; private String description; private String status; public static class TodoItemBuilder { [...] } @DynamoDBHashKey (attributeName = "Category" ) public String getCategory() { return category; } public void setCategory(String category) { this .category = category; } @DynamoDBRangeKey (attributeName = "CreationDate" ) public Date getCreationDate() { return creationDate; } public void setCreationDate(Date creationDate) { this .creationDate = creationDate; } @DynamoDBAttribute (attributeName = "Description" ) public void setDescription(String description) { this .description = description; } public String getDescription() { return description; } @DynamoDBAttribute (attributeName = "Stat" ) public String getStatus() { return status; } public void setStatus(String status) { this .status = status; } } |
Ключ хеша определяется с помощью аннотации @DynamoDBHashKey
а ключ диапазона — @DynamoDBRangeKey
. Класс builder исключен, но прост в реализации. Обратите внимание, что status
атрибута сокращен до Stat
, потому что статус — это зарезервированное ключевое слово в DynamoDB.
Теперь легко вставить некоторые тестовые данные:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
DynamoDBMapper mapper = new DynamoDBMapper(dynamodb); mapper.save( new TodoItem.TodoItemBuilder() .withCategory( "House" ) .withCreationDate(LocalDateTime.of( 2017 , 3 , 1 , 18 , 0 , 0 )) .withDescription( "Paint living room" ) .withStatus( "DONE" ).build()); mapper.save( new TodoItem.TodoItemBuilder() .withCategory( "House" ) .withCreationDate(LocalDateTime.of( 2017 , 5 , 17 , 10 , 0 , 0 )) .withDescription( "Repair kitchen sink" ) .withStatus( "OPEN" ).build()); mapper.save( new TodoItem.TodoItemBuilder() .withCategory( "House" ) .withCreationDate(LocalDateTime.of( 2017 , 9 , 13 , 9 , 0 , 0 )) .withDescription( "Clean up cellar" ) .withStatus( "DONE" ).build()); mapper.save( new TodoItem.TodoItemBuilder() .withCategory( "House" ) .withCreationDate(LocalDateTime.of( 2017 , 9 , 14 , 9 , 0 , 0 )) .withDescription( "Repair garage" ) .withStatus( "OPEN" ).build()); mapper.save( new TodoItem.TodoItemBuilder() .withCategory( "Job" ) .withCreationDate(LocalDateTime.of( 2017 , 10 , 12 , 9 , 0 , 0 )) .withDescription( "Prepare meeting" ) .withStatus( "OPEN" ).build()); mapper.save( new TodoItem.TodoItemBuilder() .withCategory( "Job" ) .withCreationDate(LocalDateTime.of( 2017 , 10 , 13 , 9 , 0 , 0 )) .withDescription( "Learn new programming language" ) .withStatus( "OPEN" ).build()); ... |
Для запроса этих данных нам нужно создать ссылку на таблицу с помощью «API документации» и передать экземпляр QuerySpec
его методу query()
:
01
02
03
04
05
06
07
08
09
10
|
DynamoDB dynamoDB = new DynamoDB(dynamodb); Table table = dynamoDB.getTable( "TodoList" ); QuerySpec spec = new QuerySpec() .withKeyConditionExpression( "Category = :category" ) .withValueMap( new ValueMap() .withString( ":category" , "Job" )) .withMaxPageSize( 10 ); ItemCollection<QueryOutcome> items = table.query(spec); |
Класс QuerySpec
позволяет нам определить условие для ключа: Category = :category
. Переменные, имеющие двоеточие, и значение для них предоставляется с использованием карты значений.
Клиентский SDK может разделить результат на страницы. withMaxPageSize()
, мы указываем, что мы хотим иметь до десяти элементов на «страницу». Следующий код перебирает эти страницы и выводит элементы:
1
2
3
4
5
6
7
8
|
int pageNumber = 0 ; for (Page<Item, QueryOutcome> page : items.pages()) { pageNumber++; System.out.println( "Page: " + pageNumber + " #####" ); for (Item item : page) { System.out.println(item.toJSONPretty()); } } |
Выполнение этого запроса приведет к выводу, подобному следующему:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
Page: 1 ##### { "CreationDate" : "2017-10-12T09:00:00.000Z" , "Category" : "Job" , "Stat" : "OPEN" , "description" : "Prepare meeting" } { "CreationDate" : "2017-10-13T09:00:00.000Z" , "Category" : "Job" , "Stat" : "OPEN" , "description" : "Learn new programming language" } |
Как мы узнали ранее, таблицы с хеш-ключом и ключом диапазона хранят данные в отсортированном порядке для одного хеш-ключа. Следовательно, эффективно запрашивать все элементы с определенным хеш-ключом, которые находятся в определенном диапазоне:
01
02
03
04
05
06
07
08
09
10
11
|
DateTimeFormatter dtf = DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" ); spec = new QuerySpec() .withKeyConditionExpression( "Category = :category and CreationDate >= :creationDate" ) .withFilterExpression( "Stat = :status" ) .withValueMap( new ValueMap() .withString( ":category" , "House" ) .withString( ":creationDate" , dtf.format(LocalDateTime.of( 2017 , 5 , 17 , 10 , 0 , 0 ))) .withString( ":status" , "OPEN" )) .withMaxPageSize( 10 ); items = table.query(spec); |
Поскольку даты хранятся с использованием строк, мы должны отформатировать экземпляр LocalDateTime
в строку, используя формат ISO-8601. Это делается с помощью DateTimeFormatter
. Ключевое условие улучшается добавлением другого предиката: and CreationDate >= :creationDate
. Значение для переменной :creationDate
предоставляется с использованием следующей карты значений.
Кроме того , мы также указать выражение фильтра на атрибуты , которые не являются частью первичного ключа: Stat = :status
. Это говорит DynamoDB, что нас интересуют только предметы с определенным статусом:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
Page: 1 ##### { "CreationDate" : "2017-05-17T10:00:00.000Z" , "Category" : "House" , "Stat" : "OPEN" , "description" : "Repair kitchen sink" } { "CreationDate" : "2017-09-14T09:00:00.000Z" , "Category" : "House" , "Stat" : "OPEN" , "description" : "Repair garage" } |
3.7 Сканирование
Сканирование позволяет нам перебирать все данные, хранящиеся в таблице. По умолчанию проверка возвращает все атрибуты элемента. Используя проекции, можно ограничить количество возвращаемых атрибутов. Один запрос на сканирование не возвращает более 1 МБ данных. Следовательно, для больших наборов данных мы должны использовать нумерацию страниц, чтобы перебрать все данные.
Следующий код демонстрирует, как перебирать все элементы в таблице TodoList
:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
ScanRequest scanRequest = new ScanRequest() .withTableName( "TodoList" ); ScanResult scan = dynamodb.scan(scanRequest); for (Map<String, AttributeValue> item : scan.getItems()){ StringBuilder sb = new StringBuilder(); for (Map.Entry entry : item.entrySet()) { if (sb.length() > 0 ) { sb.append( "; " ); } sb.append(entry.getKey()).append( "=" ).append(entry.getValue()); } System.out.println(sb.toString()); } |
Сначала мы создаем экземпляр ScanRequest
предоставить имя таблицы usinng withTableName()
. Вызов scan()
на AmazonDynamoDB
экземпляр возвращает ScanResult
.
Его метод getItems(
дает нам доступ к карте, на которой у нас есть один элемент, который AttributeValue
хранится под его хэш-ключом.
Это выводит что-то похожее на:
1
2
3
4
5
6
7
8
|
CreationDate={S: 2017-10-12T09:00:00.000Z,}; Category={S: Job,}; Stat={S: OPEN,}; description={S: Prepare meeting,} CreationDate={S: 2017-10-13T09:00:00.000Z,}; Category={S: Job,}; Stat={S: OPEN,}; description={S: Learn new programming language,} CreationDate={S: 2017-03-01T18:00:00.000Z,}; Category={S: House,}; Stat={S: DONE,}; description={S: Paint living room,} CreationDate={S: 2017-05-17T10:00:00.000Z,}; Category={S: House,}; Stat={S: OPEN,}; description={S: Repair kitchen sink,} CreationDate={S: 2017-09-13T09:00:00.000Z,}; Category={S: House,}; Stat={S: DONE,}; description={S: Clean up cellar,} CreationDate={S: 2017-09-14T09:00:00.000Z,}; Category={S: House,}; Stat={S: OPEN,}; description={S: Repair garage,} CreationDate={S: 2017-08-12T07:00:00.000Z,}; Category={S: Finance,}; Stat={S: OPEN,}; description={S: Check balance,} CreationDate={S: 2017-09-13T19:00:00.000Z,}; Category={S: Finance,}; Stat={S: OPEN,}; description={S: Sell stocks,} |
Результат сканирования в конечном итоге остается согласованным, что означает, что изменения, примененные к данным непосредственно перед операцией сканирования, могут не отражаться в наборе результатов. Он потребляет части емкости чтения таблицы в соответствии с определениями, предоставленными ранее. Для операций сканирования учитывается количество прочитанных элементов, а не размер возвращаемых данных. Следовательно, прогнозы, применяемые к набору результатов, не влияют на потребляемую мощность.
В следующем примере кода показано, как ограничить количество возвращаемых элементов выражением фильтра и размером:
01
02
03
04
05
06
07
08
09
10
|
Map<String, AttributeValue> expressionAttributeValues = new HashMap<>(); expressionAttributeValues.put( ":category" , new AttributeValue().withS( "Job" )); ScanRequest scanRequest = new ScanRequest() .withTableName( "TodoList" ) .withFilterExpression( "Category = :category" ) .withExpressionAttributeValues(expressionAttributeValues) .withProjectionExpression( "Category, Stat, description" ) .withLimit( 10 ); ScanResult scan = dynamodb.scan(scanRequest); |
Мы строим экземпляр ScanRequest
и предоставить имя таблицы и выражение фильтра: Category = :category
. Это выражение ограничивает элементы теми, которые имеют определенную категорию. Значение для привязки переменной предоставляется путем передачи a HashMap
методу withExpressionAttributeValues()
. Кроме того, мы также используем проекционное выражение для ограничения количества атрибутов каждого элемента в наборе результатов. В этом случае, мы заинтересованы только в категории и статус: Category, Stat
. Атрибуты представлены в виде строки, разделенной запятыми. Наконец, мы ограничиваем количество возвращаемых элементов до десяти, используя метод withLimit()
.
Код выше производит следующий пример вывода:
1
2
|
Category={S: Job,}; Stat={S: OPEN,}; description={S: Prepare meeting,} Category={S: Job,}; Stat={S: OPEN,}; description={S: Learn new programming language,} |
Обратите внимание, что при сканировании всегда обрабатывается полная таблица и отфильтровываются только те элементы, которые соответствуют выражению фильтра. С точки зрения производительности и потребляемой емкости чтения, это не всегда может быть подходящим; следовательно, запрос должен быть предпочтительным в большинстве случаев. С другой стороны, сканирование может быть медленнее, чем ожидалось, на очень больших таблицах, поскольку оно будет читать все элементы последовательно. Операция сканирования не выполняет автоматическое сканирование отдельных разделов параллельно. Для этого можно использовать параллельное сканирование.
Параллельное сканирование может выполняться разными потоками одного и того же процесса или даже разными процессами, запущенными одновременно. Параллельное сканирование состоит из столько сегментов, сколько у нас потоков или параллельных процессов, обращающихся к таблице. Каждый запрос на сканирование должен содержать свой номер сегмента (начиная с нуля), а также общее количество сегментов. В следующем примере мы читаем таблицу TodoList
с тремя потоками, то есть мы используем три сегмента, и каждый сегмент обрабатывает только часть таблицы:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
final int numberOfSegments = 3 ; ExecutorService executorService = Executors.newFixedThreadPool(numberOfSegments); for ( int i= 0 ; i < numberOfSegments; i++) { final int segment = i; executorService.submit( new Runnable() { @Override public void run() { Map<String, AttributeValue> lastEvaluatedKey = null ; while ( true ) { ScanRequest scanRequest = new ScanRequest() .withTableName( "TodoList" ) .withSegment(segment) .withTotalSegments(numberOfSegments) .withExclusiveStartKey(lastEvaluatedKey); ScanResult scan = dynamodb.scan(scanRequest); printScanResult(scan, segment); lastEvaluatedKey = scan.getLastEvaluatedKey(); if (lastEvaluatedKey == null ) { break ; } } } }); } executorService.shutdown(); try { executorService.awaitTermination( 1 , TimeUnit.HOURS); } catch (InterruptedException e) { executorService.shutdownNow(); } |
Прежде всего, мы создаем столько потоков, сколько у нас есть сегментов (здесь: 3). Реализация Runnable
переданного в пул потоков выдает в цикле столько запросов на сканирование, сколько нам нужно для извлечения всех данных.
Построенные ScanRequest
экземпляры заполняются именем таблицы, номером сегмента, общим количеством сегментов и ключом запуска, то есть ключом последней найденной операции сканирования. Если метод getLastEvaluatedKey()
из ScanResult
возвращений null
, мы закончили. Наконец, мы закрываем пул потоков и ждем его завершения.
3.8 Глобальные вторичные индексы
Глобальные вторичные индексы используются для индексации данных, которые не являются частью первичного ключа родительской таблицы. По своей структуре глобальные вторичные индексы аналогичны таблицам, поскольку вам также необходимо указать либо первичный ключ, состоящий только из атрибута hash, либо атрибута hash и range. Первичный ключ индекса может содержать атрибуты родительской таблицы, которые не являются частью первичного ключа родительской таблицы.
Предположим, у нас есть таблица со всеми городами мира:
Страна | город | Население города | Население столичной области |
Китай | Шанхай | 24256800 | 34750000 |
Китай | Пекин | 21516000 | 24900000 |
Нигерия | Лагос | 16060303 | 21000000 |
… |
Страна используется в качестве ключа раздела (хэш-ключа), а название города — в качестве ключа диапазона. Если мы знаем, что хотим запросить крупнейшие города в одной стране, мы можем сделать это с помощью дополнительного вторичного индекса. Этот индекс должен содержать только страну в качестве хэша и население в качестве ключа диапазона:
Страна | Население города | ||
Китай | 24256800 | ||
Китай | 21516000 | ||
Нигерия | 16060303 | ||
… |
Поскольку элементы в таблице с ключом диапазона хранятся в отсортированном виде для одного хэш-ключа, DynamoDB может просто перебирать элементы для одной страны, чтобы получить самые большие города.
Мы уже узнали, что индекс не содержит остальные атрибуты родительской таблицы, за исключением того, что мы их проецируем. Исключением из этого правила являются атрибуты, которые являются частью первичного ключа родительской таблицы. В нашем случае индекс будет также содержать атрибут city.
Теперь давайте запачкаем руки и реализуем описанный выше вариант использования. Как мы с самого начала знаем, что нам нужен глобальный вторичный индекс, мы можем указать его в самом начале при создании таблицы:
01
02
03
04
05
06
07
08
09
10
11
12
|
GlobalSecondaryIndex populationIndex = new GlobalSecondaryIndex() .withIndexName( "PopulationIndex" ) .withProvisionedThroughput( new ProvisionedThroughput() .withReadCapacityUnits(1L) .withWriteCapacityUnits(1L)) .withKeySchema( new KeySchemaElement() .withAttributeName( "Country" ).withKeyType(KeyType.HASH), new KeySchemaElement() .withAttributeName( "PopulationCity" ).withKeyType(KeyType.RANGE)) .withProjection( new Projection() .withProjectionType( "KEYS_ONLY" ) ); |
Класс GlobalSecondaryIndex
используется для создания нового глобального вторичного индекса. Мы должны указать имя индекса, предоставить значения пропускной способности (как для обычных таблиц) и определить схему ключа для него. Как упоминалось ранее, мы будем использовать в Country
качестве ключа хеша и ключа PopulationCity
диапазона. Помимо этого, мы также должны определить, какие атрибуты проецируются на индекс. Есть три разных варианта:
- KEYS_ONLY : индекс состоит только из его первичного ключа и ключа хеша и диапазона из родительской таблицы.
- ВКЛЮЧИТЬ : индекс имеет дополнительные атрибуты из родительской таблицы.
- ALL : все атрибуты из таблицы включены в индекс.
В нашем примере этот вариант KEYS_ONLY
вполне достаточен. Однако могут быть случаи, когда вам также понадобятся дополнительные атрибуты. Только представьте, что вы хотите показать в своем приложении также население большей столичной области города. В этом случае информация не может быть взята из самого индекса; следовательно, это должно быть спроецировано в него.
Теперь, когда индекс определен, мы можем создать соответствующую таблицу:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
ArrayList<AttributeDefinition> attributeDefinitions = new ArrayList<>(); attributeDefinitions.add( new AttributeDefinition() .withAttributeName( "Country" ).withAttributeType( "S" )); attributeDefinitions.add( new AttributeDefinition() .withAttributeName( "City" ).withAttributeType( "S" )); attributeDefinitions.add( new AttributeDefinition() .withAttributeName( "PopulationCity" ).withAttributeType( "N" )); ArrayList<KeySchemaElement> keySchema = new ArrayList<>(); keySchema.add( new KeySchemaElement() .withAttributeName( "Country" ).withKeyType(KeyType.HASH)); keySchema.add( new KeySchemaElement() .withAttributeName( "City" ).withKeyType(KeyType.RANGE)); CreateTableRequest createTableReq = new CreateTableRequest() .withTableName( "Cities" ) .withAttributeDefinitions(attributeDefinitions) .withKeySchema(keySchema) .withProvisionedThroughput( new ProvisionedThroughput() .withReadCapacityUnits(10L) .withWriteCapacityUnits(5L)) .withGlobalSecondaryIndexes(populationIndex); CreateTableResult result = dynamodb.createTable(createTableReq); System.out.println( "Created table " + result.toString()); |
Этот код очень похож на тот, который мы использовали ранее, за исключением того, что мы должны указать в этом случае атрибут PopulationCity
индекса в списке определений атрибутов таблицы.
Обратите также внимание на withGlobalSecondaryIndexes()
то, что при вызове передается ссылка на GlobalSecondaryIndex
экземпляр, который мы создали ранее.
Описание таблицы, которую мы выводим после ее создания, также содержит информацию о глобальном вторичном индексе:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
GlobalSecondaryIndexes: [{ IndexName: PopulationIndex, KeySchema: [{ AttributeName: Country, KeyType: HASH }, { AttributeName: PopulationCity, KeyType: RANGE }], Projection: { ProjectionType: KEYS_ONLY, }, IndexStatus: ACTIVE, ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 }, IndexSizeBytes: 0, ItemCount: 0, IndexArn: arn: aws: dynamodb: ddblocal: 000000000000: table/Cities/index/PopulationIndex }], |
Что касается таблицы, выходные данные содержат информацию о схеме ключей и предоставленной пропускной способности.
Теперь, когда индекс создан, мы можем запросить его:
1
2
3
4
5
6
7
8
9
|
DynamoDB dynamoDB = new DynamoDB(dynamodb); Table table = dynamoDB.getTable( "Cities" ); Index populationIndex = table.getIndex( "PopulationIndex" ); ItemCollection<QueryOutcome> outcome = populationIndex.query( new QuerySpec() .withKeyConditionExpression( "Country = :country and PopulationCity > :pop" ) .withValueMap( new ValueMap() .withString( ":country" , "China" ) .withLong( ":pop" , 20000000L))); printResult(outcome); |
Получив ссылку на таблицу через «Document API», мы можем вызвать ее getIndex()
для доступа к индексу. У Index
класса есть метод, query()
который можно использовать как его аналог для таблиц.
Через экземпляр QuerySpec
мы определим ключевое выражение условия: Country = :country and PopulationCity > :pop
. Это выражение выберет все города страны с населением, превышающим указанное количество.
Значения для двух переменных предоставляются через ValueMap
. В результате мы получаем два крупнейших города в Китае:
01
02
03
04
05
06
07
08
09
10
|
{ "Country" : "China" , "City" : "Beijing" , "PopulationCity" : 21516000 } { "Country" : "China" , "City" : "Shanghai" , "PopulationCity" : 24256800 } |
Обратите внимание, что выходные данные также содержат название города, хотя мы не указали его явно в индексе. Но поскольку он является частью первичного ключа таблицы, он неявно является частью индекса.
Индексы автоматически обновляются при каждом изменении таблицы. В отличие от системы реляционных баз данных, обновления индекса не происходят внутри транзакции, а выполняются асинхронно. Это означает, что индекс является только окончательно согласованным.
При нормальных условиях обновление индекса занимает всего несколько миллисекунд, поэтому в большинстве приложений вы не заметите, что индекс не содержит данных, которые были вставлены в таблицу, или наоборот, все еще содержит данные, которые были удалены из Таблица. Но так как это может произойти под большой нагрузкой, приложение должно быть подготовлено для таких ситуаций. В частности, случай, когда индекс содержит устаревшие данные, означает, что может быть невозможно получить доступ к таблице с первичным ключом, полученным из индекса.
Как мы видели выше, пропускная способность чтения и записи может быть предоставлена для индекса отдельно от таблицы. Но это также означает, что у индекса есть собственные счетчики пропускной способности. Помещение данных в таблицу также обновляет индекс. Следовательно, индекс должен иметь достаточную емкость записи. Если емкость записи индекса недостаточна, DynamoDB будет ограничивать операции записи в таблицу из-за недостаточных возможностей записи индекса. С другой стороны, запрос индекса будет ограничивать пропускную способность индекса, а не таблицу.
Глобальные вторичные индексы также можно создавать, когда таблица уже существует. В этом случае индекс должен заполняться в фоновом режиме DynamoDB данными из таблицы. Этот процесс называется «Backfilling» в DynamoDB. Поскольку «обратная засыпка» занимает много времени на больших таблицах, индекс может не стать активным сразу после его создания. Статус индекса выводится как часть операции таблицы описания.
Хотя индекс «заполняется» из существующей таблицы, может случиться, что некоторые атрибуты имеют значение, которое нельзя добавить в формате, требуемом в индексе. Например, вы определили атрибут, который имеет тип строки в таблице, но должен иметь номер типа в индексе. В этом случае DynamoDB не будет преобразовывать значение, а просто пропустит его вставку в индекс. Это означает, что в конце процесса обратной засыпки индекс max не содержит записей для всех элементов таблицы. Для выявления таких случаев нарушения индекса, DynamoDB поставляется с инструментом для их обнаружения, как его использовать, объяснено здесь .
4. Лучшие практики
4.1 Таблицы
Пропускная способность, определенная для каждой таблицы, разбита на ее разделы. Это означает, что каждый раздел имеет только n-ую часть пропускной способности полной таблицы:
1
|
Throughput Partition = Table Throughput / Partitions |
Следовательно, может случиться так, что если атрибут hash элементов не распределяет данные равномерно между разделами, таблица не достигает выделенной пропускной способности. В крайнем случае все права на чтение выполняются в одном разделе, что означает, что общий доступ является только частью доступной пропускной способности. Это приводит к рекомендации, что идентификаторы элементов должны быть выбраны так, чтобы доступ для чтения и записи был равномерно распределен по разделам.
DynamoDB автоматически создает новые разделы, если это необходимо. Количество разделов зависит от размера хранилища и настроек пропускной способности таблицы. В одном разделе может храниться до 10 ГБ данных, т. Е. DynamoDB будет разбивать данные на разделы, когда раздел станет больше 10 ГБ. Помимо этого, DynamoDB создает новый раздел, если объединенная единица мощности (CCU) на таблицу превышает ограничение в 3000 CCU. ЦБУ определяется следующим образом : CCU = 1* read-capacity + 3 * write-capacity
.
В качестве примера , если вы определили 1000 единиц емкости для чтения и 1000 единиц мощности записи, то формула выше , дает: CCU = 1 * 1000 + 3 * 1000 = 4000
. Поскольку это превышает ограничение в 3000 CCU на раздел, таблица с этими настройками емкости будет разделена на два раздела.
DynamoDB будет распределять разные разделы на разные серверные машины. Чтобы лучше использовать доступную скорость записи, имеет смысл записывать данные в последовательности, которая распределяет запись по всем разделам. Предположим, вы хотите заполнить нашу City
таблицу выше всеми городами мира. Загрузка сначала всех городов из Китая, а затем всех из Индии и т. Д. Не дает права на запись. Лучше смешать города из всех стран и загрузить первый город из Китая, затем первый город из Индии, затем из следующей страны и так далее. Таким образом, более вероятно, что запись данных происходит одновременно на разных машинах.
4.2 Предметы
Размер предмета ограничен 400 КБ. Это ограничение включает в себя имена атрибутов, то есть элемент с одним именем attr
и значениемa
будет иметь размер 5 байтов (считается как UTF-8). Поэтому имеет смысл ограничивать количество атрибутов элемента. Так как можно хранить, например, комментарии к записи блога в виде массива строк в том же элементе, что и сама запись блога, эта модель данных будет ограничивать размер и количество комментариев. В этом случае было бы более полезно иметь две таблицы: одну для всех записей в блоге и одну для всех комментариев. Обе таблицы связаны с внешним ключом. Таким образом, вы не должны загружать для каждой статьи блога все комментарии одной операцией, но вы можете ограничить количество комментариев, которые вы хотите запросить (например, ограничив их использованием даты). Кроме того, добавление нового комментария приводит к появлению новой записи в таблице комментариев, но не обновляет таблицу записями блога. В противном случае,каждый новый комментарий должен был бы обновить полную запись в блоге.
Особенно последний момент следует учитывать при распределении предметов по столам. Подумайте о хранении предметов товара. Ваша заявка будет иметь страницу, которая предоставляет некоторую основную информацию о продукте. Кроме того, вам может потребоваться сохранить его доступность и рейтинг пользователя. Поскольку последние два элемента меняются чаще, чем основное описание, имеет смысл создать три таблицы вместо одной большой таблицы. В то время как первая таблица хранит только основные атрибуты, вторая связана с управлением доступными товарными позициями, а третья хранит рейтинги. Обновление доступности не должно обновлять полное описание продукта, ни оценки. Это не только более эффективно с точки зрения передачи по сети, но и потребляет меньше емкости.
Что касается затрат на хранение, также стоит сжать контент там, где это имеет смысл. Взяв, к примеру, длинное описание продукта из нашего предыдущего примера, оно может потреблять меньше памяти, если оно сжато. Это, конечно, имеет обратную сторону: каждый раз, когда он должен отображаться, он должен быть распакован. Однако, имея тысячи описаний продуктов в одной базе данных, этот метод может уменьшить размер хранилища и, соответственно, время поиска, введя немного больше времени вычислений.
4.3 Запрос и сканирование
Операция сканирования может считывать до 1 МБ данных из одного раздела. Это не только потребляет много доступной емкости для чтения одновременно, но также ограничивает использование емкости одним разделом. Это может привести к блокировке DynamoDB после запросов на чтение в той же таблице / разделе. В этом случае может быть полезно ограничить размер операции сканирования, используя ее метод withLimit()
:
1
2
|
ScanRequest scanRequest = new ScanRequest() .withTableName( "TodoList" ).withLimit( 10 ); |
Приведенный выше код ограничивает количество элементов, обрабатываемых операцией сканирования, до 10. Если средний размер элементов составляет около 800 байт, это сканирование не будет считывать более 8 КБ, т. Е. Будет занимать две возможные последовательные операции чтения по 4 КБ. (= одна единица емкости чтения).
Это дает следующим запросам достаточно игровой площадки для выполнения без удушения.
Вместо того, чтобы выполнять сканирование в той же таблице, что и запросы, можно также подумать о репликации данных в разные таблицы. Одна таблица обслуживает критичные ко времени запросы, а другая используется для выполнения длительных операций сканирования.
4.4 Индексы
Как и в системах реляционных баз данных, индексы могут повысить производительность запросов, увеличив размер используемого хранилища и необходимые операции ввода-вывода для хранения и обновления данных. Следовательно, индексы должны использоваться экономно только для данных, которые часто запрашиваются.
Кроме того, индексы должны содержать только те данные, которые часто запрашиваются. Проецирование слишком большого количества атрибутов из каждого элемента в индекс приводит к большим индексам, которые занимают больше места и менее эффективны для запроса. Может быть более эффективно запрашивать атрибут непосредственно из таблицы, если он используется нечасто, а не проецируется в индекс. С другой стороны, все атрибуты, к которым часто обращаются в запросах, должны проецироваться в индекс, так как в противном случае DynamoDB может потребоваться извлечь их из таблицы, прочитав полный элемент. Это вводит дополнительный ввод / вывод и потребляет единицы чтения.
Другой метод ускорения индексов заключается в использовании «разреженных индексов». Так как DynamoDB будет создавать записи в индексе, только если соответствующий элемент действительно имеет значения для атрибута hash и range. Эти знания можно использовать для создания таблицы, которая помечает, например, все продукты, которых нет на складе, с помощью специальной строки в выделенном столбце. Продукты, которых нет в наличии, не имеют этого атрибута. Определяя индекс по идентификатору продукта (хэш) и символу отсутствия на складе (диапазон), индекс будет содержать записи только для продуктов, которых нет в наличии.
5. Резервное копирование
Для создания резервных копий данных, хранящихся в DynamoDB, можно использовать другую веб-службу Amazon (AWS): AWS Data Pipeline . Служба конвейера данных позволяет указать процессы, которые перемещают данные между различными
службами AWS. В списке поддерживаемых сервисов у нас есть DynamoDB, а также Amazon S3. Это позволяет нам создавать процесс, который экспортирует данные из таблицы DynamoDB в корзину Amazon S3 с заданными интервалами.
Поскольку полное описание службы AWS Data Pipeline выходит за рамки этого учебного пособия, мы просто хотим упомянуть, что вы можете открыть URL https://console.aws.amazon.com/datapipeline/
в своем браузере и нажать «Создать новый конвейер». Далее вы можете указать имя и описание по желанию. Для Source вы можете выбрать шаблон Export DynamoDB table to S3
и указать под параметрами имя таблицы и область вывода S3. Более подробное описание можно найти, например, здесь .
6. Ценообразование
Amazon предоставляет бесплатный уровень, который позволяет обрабатывать до 200 миллионов запросов в месяц (25 единиц емкости записи и 25 единиц емкости чтения) и предоставляет 25 ГБ индексированных хранилищ данных. Нужно платить только за ресурсы, которые потребляются за пределами этого бесплатного уровня.
За ресурсы, выходящие за пределы этого бесплатного уровня, в настоящее время необходимо заплатить 0,000725 долларов США за единицу емкости записи (WCU) в час и 0,000145 долларов США за единицу емкости чтения (RCU) в час в регионе Запад США (Северная Калифорния).
В пересчете на месячную цену это около $ 0,52 за WCU и $ 0,10 за RCU. В других регионах мира эти цены могут отличаться. Например, в регионе ЕС (Франкфурт) платят 0,000793 долл. США за WCU и 0,0001586 долл. США за RCU.
Цена за 1 ГБ хранилища индексированных данных в настоящее время находится в регионе Запад США (Северная Калифорния) $ 0,28 за ГБ-месяц.
Хотя угадать правильную емкость для каждой таблицы иногда сложно, Amazon предоставляет функцию «Автоматическое масштабирование», которая автоматически увеличивает или уменьшает единицы емкости записи и чтения для каждой таблицы. Автоматическое масштабирование включено по умолчанию и использует целевое использование 70%. Это означает, что он пытается сохранить единицы емкости записи и чтения для каждой таблицы на уровне около 70% выделенной емкости, но всегда между минимальным и максимальным указанным значением. Так, если вы укажете, например,
100 RCU и 100 WCU как минимум и 4000 RCU и 4000 WCU как максимум, автоматическое масштабирование не будет масштабировать таблицу ниже или выше этих значений емкости. Но это позволит скорректировать выделенную пропускную способность, если она увеличится более чем на 70 RCU и 70 WCU (70%).
7. Загрузите исходный код
Это был учебник по Amazon DynamoDB Ultimate.
Вы можете скачать полный исходный код этого примера здесь: Amazon DynamoDB Ultimate Tutorial