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 的頂層 key,不是 e.response["Error"] 底下的。這就是為什麼程式碼寫的是 e.response.get("Item")。很容易跑去 ["Error"] 裡跟 CodeMessage 一起找,結果什麼都沒找到,然後斷定這個參數沒作用。
  • 項目回來時是 DynamoDB JSON,即使你可能已經習慣原生值,因為這是低階用戶端。若你想要單純的 Python 值,boto3.dynamodb.types.TypeDeserializer 可以轉換它。
  • 資源 API 用物件表達同一個防護ConditionExpression=Attr("Version").eq(7) & Attr("Artist").exists(),使用原生值、不需佔位符對應。它拋出的是完全相同的例外,所以下面的處理方式不變。
  • 檢查失敗照樣計費一次寫入。開發人員指南明確說明:條件為 false 仍會消耗寫入容量,並以新舊項目中較大的那一個計算大小,所以在爭用的鍵上無上限地重試會花掉真金白銀卻毫無進展。

boto3 把回傳的項目放在哪裡

對一個已存 Version 為 9 的項目執行上面的程式碼,然後印出例外回應的 keys。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 這個 key 不存在,而 e.response.get("Item") 會回傳 None 而不是拋錯。這正是那種能通過程式碼審查、然後開始在正式環境記錄 None 的錯誤版本。

為什麼運算式裡每個名稱都要別名

程式碼寫的是 #version#cond0 而不是 VersionArtist,對兩個平凡的英文字來說看起來太過頭了。對這兩個字來說確實如此。Version 不是 DynamoDB 保留字,直接寫也能通過名稱驗證。

Year 是保留字,而同一張表格裡就有一個。直接對它下防護,你會得到:

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

那份清單上有 573 個字,包括 NameStatusSizeCountDataOwnerTimestampItems。把所有名稱都別名化,正是產生器產出的程式碼永遠不必分辨誰是誰的方法。把你的屬性名稱貼進保留字檢查器,它會替需要的那些回傳 ExpressionAttributeNames 對應。

若想在自己的表格上寫這些防護、又讓別名化自動處理好,請下載 DynoTable

相關範例

參考資料

最後驗證於 2026-07-28,對照上方連結的 AWS 官方文件。

不必透過主控台就能操作 DynamoDB

一款快速的 DynamoDB 桌面用戶端,可執行 DynamoDB 無法執行的真正 SQL — JOINs、GROUP BY、聚合 — 並支援視覺化編輯與使用你自己的 Bedrock 金鑰的 AI 代理。

30 天免費試用,無需信用卡 — 之後為無時間限制的免費方案。