AWSが提供するフルマネージドNoSQLデータベースサービスであるDynamoDBには、データ更新の方法が複数ある。DynamoDBを気軽に導入していくと、ビジネスの要求によってはDynamoDBに向かないデータ更新を選択せざるを得なくなることになってしまう。そういった状況に陥いらないためには、どのような要求が有り得るのか、もしくはどういった要求は有り得ないのかといったことを、事前に洗い出して検討しておく必要がある。また、それら要求の変化の幅に加えて、DynamoDBは何が不得意なのかを知っておく必要もある。今回は、DynamoDBの操作の中でもデータの更新処理に焦点を当てて、何が不得意なのかという事について考えることにする。
この文章は自分用の検討材料のためにまとめたものであり、内容については正確性を保つようには努めるが確実なものではない。そのため、必要にせまられてこの文章に辿りついたのであれば、実際にそれらを動かすなどして観察してみると良いと思う。その際の参考資料になれば嬉しい。
簡単な項目の登録
DynamoDBのテーブルに登録するデータを公式のドキュメントでは項目と呼んでいるため、ここでも項目という表現を使う。アイテムやItemと表現されているデータのことで、RDBMSではレコードと表現されているものに相当する。
項目の登録にはPutItemアクションを使う1。簡単なPutItem用の入力ファイルには、テーブル名と登録したい値を指定する。値の指定方法は、DynamoDBの独自の書き方をするので注意が必要だ。
{
"TableName": "foo",
"Item": {
"id": {
"S": "testing"
}
}
}
この入力ファイルを使い、PutItemを実行する。
aws dynamodb put-item --cli-input-json file://put-item.json
実行が完了すると、項目が1つ追加される。もし、同じプライマリーキーをもつ項目がすでにある場合は、項目が置き換えられる。
項目の登録する条件を指定する
項目の登録や更新や削除は、特定の条件を満した場合のみにその処理を行うということを指定できる。条件を満さない場合は、エラーとなる。これらの条件はDynamoDB独自の条件書式を使用して指定する2。これを使用することで、排他制御のような機構を実現できる3。ここでは、条件付き配置の記法を使用し項目の登録をする。
先程、fooテーブルにtestingというキーの項目を追加した。では、同じキーが存在しなければ追加するという条件付き配置で、同じキーの項目を追加するとどうなるか試す。
{
"TableName": "foo",
"Item": {
"id": {
"S": "testing"
}
"name": {
"S": "cccc"
}
},
"ConditionExpression": "attribute_not_exists(id)"
}
この入力ファイルを使い、PutItemを実行する。
aws dynamodb put-item --cli-input-json file://put-item-write-with-condition-fail.json
既にidにtestingという値を持つ項目がfooテーブルに存在するため、このコマンドは失敗する。
An error occurred (ConditionalCheckFailedException) when calling the PutItem operation: The conditional request failed
ではidの値を変更し、PutItemしてみる。
{
"TableName": "foo",
"Item": {
"id": {
"S": "yay"
}
},
"ConditionExpression": "attribute_not_exists(id)"
}
この入力ファイルを使い、PutItemを実行する。
aws dynamodb put-item --cli-input-json file://put-item-write-with-condition-success.json
同じidをもつ値が存在しないため、このコマンドは成功し、項目が追加される。
各言語やライブラリでの実装
条件付き配置の記法と、失敗した場合にどのようなエラーが発生するのかを、各種言語やライブラリの実装で確認する。
Boto3の場合
Boto3を使い、条件付き配置の記法と、発生するエラーを確認する。
実行すると botocore.errorfactory.ConditionalCheckFailedException
が発生する。
Traceback (most recent call last): File "example_put_item_boto3.py", line 11, in <module> rc = dynamodb_client.put_item( File "/var/venv/lib/python3.8/site-packages/botocore/client.py", line 530, in _api_call return self._make_api_call(operation_name, kwargs) File "/var/venv/lib/python3.8/site-packages/botocore/client.py", line 960, in _make_api_call raise error_class(parsed_response, operation_name) botocore.errorfactory.ConditionalCheckFailedException: An error occurred (ConditionalCheckFailedException) when calling the PutItem operation: The conditional request failed
PynamoDBの場合
DynamoDB用O/R MapperであるPynamoDBを使用し、条件付き配置で条件を満さない場合のエラーを確認する。余談だがDynamoDB自体がリレーショナルではないため、O/R Mapperという表現が正しくないように感じる。だとするとどのように表現するのが良いのだろうか。DynamoDBをDjango ORMっぽく扱うためのライブラリというのが良く言い表しているようにも思えるけれど、なんかイマイチだ。
実行すると pynamodb.exceptions.PutError
が送出される。
Traceback (most recent call last): File "/var/venv/lib/python3.8/site-packages/pynamodb/connection/base.py", line 1023, in put_item return self.dispatch(PUT_ITEM, operation_kwargs, settings) File "/var/venv/lib/python3.8/site-packages/pynamodb/connection/base.py", line 329, in dispatch data = self._make_api_call(operation_name, operation_kwargs, settings) File "/var/venv/lib/python3.8/site-packages/pynamodb/connection/base.py", line 440, in _make_api_call raise VerboseClientError(botocore_expected_format, operation_name, verbose_properties) pynamodb.exceptions.VerboseClientError: An error occurred (ConditionalCheckFailedException) on request (MASK) on table (foo) when calling the PutItem operation: The conditional request failed During handling of the above exception, another exception occurred: Traceback (most recent call last): File "example_put_item_pynamodb.py", line 13, in <module> rc = t.save(condition=Testing.id.does_not_exist()) File "/var/venv/lib/python3.8/site-packages/pynamodb/models.py", line 439, in save data = self._get_connection().put_item(*args, **kwargs) File "/var/venv/lib/python3.8/site-packages/pynamodb/connection/table.py", line 150, in put_item return self.connection.put_item( File "/var/venv/lib/python3.8/site-packages/pynamodb/connection/base.py", line 1025, in put_item raise PutError("Failed to put item: {}".format(e), e) pynamodb.exceptions.PutError: Failed to put item: An error occurred (ConditionalCheckFailedException) on request (MASK) on table (foo) when calling the PutItem operation: The conditional request failed
準備
DynamoDBの操作を実行するには当然DynamoDBのテーブルが必要になる。今回はAWS ConsoleからDynamoDBのテーブルを1つ作成した。またそのテーブルにアクセス可能な必要な権限をテスト用のアカウントに設定した。
(setenv "TABLE_NAME" "testing")
テスト用にいくつかのItemを事前に登録した。
aws dynamodb put-item --table-name ${TABLE_NAME} --item '{"id":{"S":"foo"},"itemType":{"N":"0"}}'
aws dynamodb put-item --table-name ${TABLE_NAME} --item '{"id":{"S":"bar"},"itemType":{"N":"1"}}'
以下のようなデータを登録した。
aws dynamodb scan --table-name ${TABLE_NAME}
{ "Items": [ { "id": { "S": "bar" }, "itemType": { "N": "1" } }, { "id": { "S": "foo" }, "itemType": { "N": "0" } } ], "Count": 2, "ScannedCount": 2, "ConsumedCapacity": null }
UpdateItem
通常DynamoDBのアイテムの更新にはUpdateItemを用いる。
登録したアイテムを取得してデータを確認する。
条件付きUpdateItem
UpdateItemは特定の条件を満す時にUpdateItemを実行できる。以下ではitemTypeが1の時に更新処理を行うようにしている。
登録したアイテムを取得してデータを確認する。
BatchWriteItem
一括でレコードを更新するのであればBatchWriteItemという方法もある。ただし以下のような制限がある。AWSのBatchWriteItemに関するドキュメントを引用する4。
翻訳すると以下のようになる。
次の条件が1つでも当てはまる場合、DynamoDBはバッチ書き込み操作全体を拒否する。
- BatchWriteItemリクエストで存在しないテーブルがひとつでも指定された。
- リクエストのアイテムに指定された主キー属性が、対応するテーブルの主キースキーマの属性と一致しない。
- 同じBatchWriteItemリクエストの同じアイテムに対して複数の操作を実行しようとした。 同じBatchWriteItemリクエストに同じアイテムを入れたり削除したりするこ とはできない。
- リクエストには、同じハッシュキーとレンジキーを持つアイテムが少なくとも2つ含まれている。 これは基本的に2つのプット操作となる。
- バッチには25を超えるリクエストがある。
- バッチ内の個々のアイテムのサイズが400KBを超えている。
- リクエストの合計サイズが16MBを超えている。
なかなか厳しい制約がある。
トランザクション
DynamoDBはトランザクションをサポートしているが、RDBMSのトランザクションとは多少様子が異なる5。
TransactWriteItems - トランザクションでの書き込み
トランザクションでの書き込み処理には TransactWriteItems
を使う6。
{
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
},
{
"AttributeName": "deptno",
"AttributeType": "N"
},
{
"AttributeName": "empno",
"AttributeType": "N"
}
],
"TableName": "testing",
"KeySchema": [
{
"AttributeName": "deptno",
"KeyType": "HASH"
},
{
"AttributeName": "id",
"KeyType": "RANGE"
}
],
"LocalSecondaryIndexes": [
{
"IndexName": "testing_local_index",
"KeySchema": [
{
"AttributeName": "deptno",
"KeyType": "HASH"
},
{
"AttributeName": "empno",
"KeyType": "RANGE"
}
],
"Projection": {
"ProjectionType": "ALL"
}
}
],
"BillingMode": "PROVISIONED",
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
},
"TableClass": "STANDARD"
}
このテーブルに3つのデータを登録する。
3つのデータを登録する例(aws-dynamodb-transact-write-items-first.json)
{
"TransactItems": [
{
"Put": {
"TableName": "testing",
"Item": {
"id": {"S": "a"},
"deptno": {"N": "1"},
"empno": {"N": "100"}
}
}
},
{
"Put": {
"TableName": "testing",
"Item": {
"id": {"S": "b"},
"deptno": {"N": "1"},
"empno": {"N": "101"}
}
}
},
{
"Put": {
"TableName": "testing",
"Item": {
"id": {"S": "c"},
"deptno": {"N": "2"},
"empno": {"N": "100"}
}
}
}
]
}
実行する。
aws dynamodb transact-write-items --cli-input-json file://aws-dynamodb-transact-write-items-first.json
これは登録できる。
RDBMS
のトランザクションと比べて大きく違うところは、同一アイテム(レコード)に対しての複数の操作を、トランザクション内で実行できない事だろう。あるアイテムに対して2回更新するような処理は、1度のトランザクションでは実行できない。また、あるアイテムを削除し、同じキーを持つアイテムを登録するといった操作もできない。そういった操作を実施したい場合は、処理を分ける必要がある。
3つのデータを削除し入れ直そうとするが、できない例(aws-dynamodb-transact-write-items-second.json)
{
"TransactItems": [
{
"Delete": {
"TableName": "testing",
"Key": {
"id": {"S": "a"},
"deptno": {"N": "1"}
}
}
},
{
"Delete": {
"TableName": "testing",
"Key": {
"id": {"S": "b"},
"deptno": {"N": "1"}
}
}
},
{
"Delete": {
"TableName": "testing",
"Key": {
"id": {"S": "c"},
"deptno": {"N": "2"}
}
}
},
{
"Put": {
"TableName": "testing",
"Item": {
"id": {"S": "a"},
"deptno": {"N": "1"},
"empno": {"N": "1001"}
}
}
},
{
"Put": {
"TableName": "testing",
"Item": {
"id": {"S": "b"},
"deptno": {"N": "1"},
"empno": {"N": "1011"}
}
}
},
{
"Put": {
"TableName": "testing",
"Item": {
"id": {"S": "c"},
"deptno": {"N": "2"},
"empno": {"N": "1001"}
}
}
}
]
}
これは実行できない。このような操作をする場合は処理を分ける必要がある。またこの処理の中で、インデックスを指定して削除するといった操作もできない。
PartiQLを用いて複数レコードをUPDATEする
PartiQLを用いたとしてもSQLのように複数レコードを一気にUPDATEはできない。ただしバッチ処理として1行ずつ更新するクエリを複数送信することで、似たようなことはできる。ただし似てはいるかもしれないが、本質的には一括更新ではないし、複数のクエリを組み立ててリクエストを送信する必要がある。