Python(boto3)での DynamoDB TransactWriteItems

トランザクションは、boto3 の 2 つの API が最も激しく分かれる場面の 1 つです。transact_write_items は低レベルクライアントにしか存在しないので、Table が与えてくれるネイティブ Python 型の便利さはここでは使えません。そしてトランザクションが失敗したとき、必要なものは、boto3 のコードがまず覗かない例外の片隅にあります。(トランザクションが何を買ってくれるのかは、どの SDK でも同じです。)

コード

import boto3

client = boto3.client("dynamodb")

# Move one award between two songs — atomically. If the first song has no
# award to give, NEITHER update happens.
try:
    client.transact_write_items(
        TransactItems=[
            {
                "Update": {
                    "TableName": "Music",
                    "Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
                    "UpdateExpression": "SET #upd0 = #upd0 - :one",
                    "ConditionExpression": "#upd0 >= :one",
                    "ExpressionAttributeNames": {"#upd0": "Awards"},
                    "ExpressionAttributeValues": {":one": {"N": "1"}},
                }
            },
            {
                "Update": {
                    "TableName": "Music",
                    "Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "A Mis Abuelos"}},
                    "UpdateExpression": "SET #upd0 = if_not_exists(#upd0, :zero) + :one",
                    "ExpressionAttributeNames": {"#upd0": "Awards"},
                    "ExpressionAttributeValues": {":one": {"N": "1"}, ":zero": {"N": "0"}},
                }
            },
        ]
    )
    print("Transaction committed")
except client.exceptions.TransactionCanceledException as e:
    # One reason per action, in TransactItems order. Code "None" means that
    # action was fine — some OTHER action sank the transaction.
    codes = [reason["Code"] for reason in e.response["CancellationReasons"]]
    print(f"Transaction canceled: {codes}")  # e.g. ['ConditionalCheckFailed', 'None']

解説

  • TransactItemsPutUpdateDeleteConditionCheck の dict のリストで、すべての値が DynamoDB JSON、例外はありません。型付きの形が任意でない唯一の boto3 呼び出しがこれであり、このページの末尾のセクションが存在する理由でもあります。上限は CLI のページにあります。
  • CancellationReasonsError の中にはありません。botocore はモデル化されたエラーフィールドをレスポンス dict のトップに持ち上げるので、捕捉した例外が携える e.response には CancellationReasonsErrorMessageResponseMetadata が横並びで入っています。e.response["Error"] の下を探しても何も見つかりません。e.response["Error"] が保持しているのは要約のコードとメッセージだけです。
  • None のエントリに "Message" はありません — 成功したアクションの理由はキー 1 つの dict {"Code": "None"} なので、自然に書いた [r["Message"] for r in reasons] は、よりによって成功したアクションで KeyError: 'Message' を送出します。r.get("Message") を使いましょう。
  • 生成された例外クラス — botocore は client.exceptions.TransactionCanceledException をサービスモデルから実行時に構築します。だからそれはクライアントインスタンスにぶら下がっており、from botocore.exceptions import ... できないのです。クライアントがスコープに無いヘルパーでは、botocore.exceptions.ClientError を捕捉して e.response["Error"]["Code"] で分岐しましょう。生成されたクラスはそのサブクラスです。
  • 構造的な誤りはキャンセルとしては届かない ので、スニペットの except 節はそれらを一度も見ません。同じアイテムを狙った 2 つのアクションは、コードが ValidationExceptione.responseCancellationReasons キーを持たない素の ClientError を送出します。どのアクションも実行される前にトランザクションが拒否されたからです。それらも同じ文脈でログに残したいなら、外側で ClientError を捕捉しましょう。
  • アクションに付けた ReturnValuesOnConditionCheckFailure: "ALL_OLD" は、そのアクションの理由の Item キーの下に、負けたアイテムを DynamoDB JSON で入れてくれます。競り負けたあとの追加の get_item が要らなくなります。
  • ClientRequestToken は boto3 が埋めてくれます。ワイヤ上で捕捉したところ、同一内容の transact_write_items 呼び出し 2 回は別々の UUID を持って出ていきました。つまりトークンがカバーするのは単一の呼び出しであって、自前の catch-and-retry ループではありません。リトライがプロセスより長生きしうるなら、安定したトークンを自分で渡しましょう。
  • TransactionConflict ではリトライし、ConditionalCheckFailed では決してリトライしない — 前者は他の誰かが一瞬アイテムを握っていたと告げ、後者は前提条件が偽であり次回も偽のままだと告げます。たいていのハンドラーが区別する必要があるのはこの 2 つのコードだけで、全体は TransactionCanceledException のページで読み解いています
  • コスト — トランザクション書き込みは、同じ書き込みをトランザクションの外で行う場合のおよそ 2 倍を課金します(CLI のページで計測しています)。単一アイテムのアトミック性だけが必要なら、条件付き書き込みがそれを半額で買ってくれます。

これにリソース API 版はありません

boto3.resource("dynamodb").Table(...)transact_write_items 属性はありません。持っているのは resource.meta.client だけです。したがって Table とネイティブ Python 型に落ち着いたコードベースも、トランザクションのためには型付きの DynamoDB JSON に戻るか、boto3.dynamodb.types.TypeSerializer で手作業でシリアライズするしかありません。

from boto3.dynamodb.types import TypeSerializer

serialize = TypeSerializer().serialize
values = {k: serialize(v) for k, v in {":one": 1, ":zero": 0}.items()}

TypeSerializer はリソース API と同じルールを適用します。つまり float を拒否し、小数には decimal.Decimal を求めます。DynamoDB JSON コンバーターは、リテラルをスクリプトに貼りたいだけのときに、同じ変換をブラウザーで行います。トランザクションが触れるアイテムを、どちらの形も手書きせずに編集するには、DynoTable をダウンロードしてください。

関連する例

参考資料

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

Console なしで DynamoDB を扱う

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

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