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") と読んでいるのはそのためです。CodeMessage の隣、["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 をログに吐き始めるタイプのバグです。

式の中の名前をすべてエイリアスする理由

フェンスは VersionArtist ではなく #version#cond0 を書いていて、ありふれた 2 語にしては大げさに見えます。この 2 つに関しては、確かに大げさです。Version は DynamoDB の予約語ではなく、そのまま使っても名前の検証を通ります。

Year は予約語で、同じテーブルにその属性があります。これを直接ガードに使うとこうなります。

ValidationException: Invalid ConditionExpression: Attribute name is a reserved keyword;
reserved keyword: Year

そのリストには 573 語が載っており、NameStatusSizeCountDataOwnerTimestampItems も含まれます。すべてをエイリアスするのは、生成コードがどれがどれかを一切知らずに済ませるやり方です。属性名を予約語チェッカーに貼り付ければ、必要なものについて ExpressionAttributeNames のマップを返してくれます。

エイリアス処理を任せたまま、自分のテーブルに対してこうしたガードを書くには、DynoTable をダウンロードしてください。

関連する例

参考資料

最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。