« ^ »

DynamoDBのデータ更新処理方法をいくつか検討する

所要時間: 約 8分

TL;DR

  • DynamoDBのデータの更新についてUpdateItem、条件付きUpdateItem、BatchWriteItemといった方法があり、それぞれの挙動を確認した。
  • DynamoDBのデータの扱いには、件数やサイズに制限があるため注意が必要なことがわかった。
  • この他にPartiQLを用いることもできる。


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。ここでは、条件付き配置の記法を使用し項目の登録をする。

attribute_not_exists(Id)
条件付き配置の書き方

先程、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 が送出される。

条件付き配置で条件を満さない場合には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を用いる。

aws dynamodb update-item --table-name ${TABLE_NAME} --key '{"id": {"S": "foo"}}' \
    --update-expression 'SET itemType = :ok' \
    --expression-attribute-values '{":ok": {"N": "1"}}'
UpdateItem

登録したアイテムを取得してデータを確認する。

aws dynamodb get-item --table-name ${TABLE_NAME} --key '{"id": {"S": "foo"}}'
{
    "Item": {
        "id": {
            "S": "foo"
        },
        "itemType": {
            "N": "1"
        }
    }
}
GetItem

条件付きUpdateItem

UpdateItemは特定の条件を満す時にUpdateItemを実行できる。以下ではitemTypeが1の時に更新処理を行うようにしている。

aws dynamodb update-item --table-name ${TABLE_NAME} --key '{"id": {"S": "foo"}}' \
    --update-expression 'SET itemType = :ng' \
    --condition-expression 'itemType = :ok' \
    --expression-attribute-values '{":ok": {"N": "1"}, ":ng": {"N": "0"}}'
UpdateItem

登録したアイテムを取得してデータを確認する。

aws dynamodb get-item --table-name ${TABLE_NAME} --key '{"id": {"S": "foo"}}'
{
    "Item": {
        "id": {
            "S": "foo"
        },
        "itemType": {
            "N": "0"
        }
    }
}
GetItem

BatchWriteItem

一括でレコードを更新するのであればBatchWriteItemという方法もある。ただし以下のような制限がある。AWSのBatchWriteItemに関するドキュメントを引用する4

If one or more of the following is true, DynamoDB rejects the entire batch write operation:

  • One or more tables specified in the BatchWriteItem request does not exist.
  • Primary key attributes specified on an item in the request do not match those in the corresponding table's primary key schema.
  • You try to perform multiple operations on the same item in the same BatchWriteItem request. For example, you cannot put and delete the same item in the same BatchWriteItem request.
  • Your request contains at least two items with identical hash and range keys (which essentially is two put operations).
  • There are more than 25 requests in the batch.
  • Any individual item in a batch exceeds 400 KB.
  • The total request size exceeds 16 MB.
BatchWriteItemを引用

翻訳すると以下のようになる。

次の条件が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行ずつ更新するクエリを複数送信することで、似たようなことはできる。ただし似てはいるかもしれないが、本質的には一括更新ではないし、複数のクエリを組み立ててリクエストを送信する必要がある。

脚注