Python (boto3) での DynamoDB 条件付き書き込み
boto3 は、条件付き書き込みに対して捕捉すべき名前付きの例外クラスがある唯一の SDK であり、返ってくるアイテムが思いもよらない場所に隠れている唯一の SDK でもあります。式そのものはどこでも同じように動きます。関数と楽観的ロックのパターンは DynamoDB の条件式が扱います。
コード
import boto3
client = boto3.client("dynamodb")
# Update the item only if nobody changed it since we read version 7.
try:
client.update_item(
TableName="Music",
Key={"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
UpdateExpression="SET #upd0 = :updValue0, #version = :newVersion",
ConditionExpression="attribute_exists(#cond0) AND #version = :expectedVersion",
ExpressionAttributeNames={"#upd0": "Genre", "#version": "Version", "#cond0": "Artist"},
ExpressionAttributeValues={
":updValue0": {"S": "Latin Jazz"},
":expectedVersion": {"N": "7"},
":newVersion": {"N": "8"},
},
ReturnValuesOnConditionCheckFailure="ALL_OLD",
)
print("Updated to version 8")
except client.exceptions.ConditionalCheckFailedException as e:
# With ReturnValuesOnConditionCheckFailure="ALL_OLD", the current item
# rides back on the exception — no extra read to see what beat you.
print("Lost the race — item is now:", e.response.get("Item"))解説
ConditionalCheckFailedExceptionはモデル化されたクラスなので、except client.exceptions.…が使えます。DynamoDB のエラーの大半はそうではありません。ValidationExceptionにはクラスがまったくなく、e.response["Error"]["Code"]で照合するしかありません。モデル化されたクラスもやはりClientErrorのサブクラスなので、ハンドラーの順序を雑にすると、上流の広いexcept ClientErrorが飲み込んでしまいます。- 返ってくるアイテムは
e.responseのトップレベルのキーであり、e.response["Error"]の下ではありません。フェンスがe.response.get("Item")と読んでいるのはそのためです。CodeやMessageの隣、["Error"]の下を探しに行き、何も見つからず、パラメータが効かなかったと結論づけてしまいがちです。 - アイテムは DynamoDB JSON で返ってきます。ネイティブな値に慣れているかもしれませんが、これは低レベルのクライアントだからです。素の Python が欲しければ
boto3.dynamodb.types.TypeDeserializerが変換してくれます。 - リソース API は同じガードをオブジェクトとして表現します。
ConditionExpression=Attr("Version").eq(7) & Attr("Artist").exists()のように、ネイティブな値でプレースホルダーのマップなしに書けます。送出される例外は同一なので、以下の扱い方は変わりません。 - チェックの失敗にもやはり書き込み分が課金されます。デベロッパーガイドは、条件が false なら書き込みキャパシティを消費し、その大きさは古いアイテムと新しいアイテムの大きい方で決まると明示しています。競合するキーに対する上限なしのリトライは、進捗ゼロのまま実際のお金を使います。
boto3 は返ってきたアイテムをどこに置くか
保存された Version が 9 の状態でフェンスを実行し、例外のレスポンスのキーを表示してみます。DynamoDB Local 3.3.0、boto3 1.43.58 での結果です。
sorted(e.response.keys()) -> ['Error', 'Item', 'ResponseMetadata']
e.response["Item"] -> {'Artist': {'S': 'Arturo Sandoval'},
'Year': {'N': '1994'},
'Version': {'N': '9'},
'SongTitle': {'S': 'Cubano Chant'},
'AlbumTitle': {'S': 'Danzon'}}ReturnValuesOnConditionCheckFailure を外すと、同じ失敗が ['Error', 'ResponseMetadata'] を返します。Item キーは存在せず、e.response.get("Item") は例外を投げるのではなく None を返します。これが、コードレビューを生き延びて本番で None をログに吐き始めるタイプのバグです。
式の中の名前をすべてエイリアスする理由
フェンスは Version や Artist ではなく #version と #cond0 を書いていて、ありふれた 2 語にしては大げさに見えます。この 2 つに関しては、確かに大げさです。Version は DynamoDB の予約語ではなく、そのまま使っても名前の検証を通ります。
Year は予約語で、同じテーブルにその属性があります。これを直接ガードに使うとこうなります。
ValidationException: Invalid ConditionExpression: Attribute name is a reserved keyword;
reserved keyword: Yearそのリストには 573 語が載っており、Name、Status、Size、Count、Data、Owner、Timestamp、Items も含まれます。すべてをエイリアスするのは、生成コードがどれがどれかを一切知らずに済ませるやり方です。属性名を予約語チェッカーに貼り付ければ、必要なものについて ExpressionAttributeNames のマップを返してくれます。
エイリアス処理を任せたまま、自分のテーブルに対してこうしたガードを書くには、DynoTable をダウンロードしてください。
関連する例
- Node.js での DynamoDB 条件付き書き込み — AWS SDK v3 での同じ楽観的ロック。
- AWS CLI での DynamoDB 条件付き書き込み — シェルからの同じ楽観的ロック。
- Python での DynamoDB PutItem — 作成のみの
attribute_not_existsput。 - DynamoDB の条件式 — すべての関数とパターン。
- 複数属性での一意性の強制 — 条件とトランザクションの組み合わせ。
- DynamoDB ConditionalCheckFailedException — チェックの失敗が想定内のときに、それを安く扱う方法。
参考資料
- UpdateItem — Amazon DynamoDB API Reference
- DynamoDB.Client.update_item — Boto3 documentation
- Condition expressions — Amazon DynamoDB Developer Guide
- DynamoDB read and write operations (capacity unit consumption) — Amazon DynamoDB Developer Guide
- Reserved words in DynamoDB — Amazon DynamoDB Developer Guide
最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。