Python(boto3)中的 DynamoDB 条件写入

boto3 是唯一一个给条件写入配了具名异常类可供 catch 的 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") 的原因。很容易顺着 ["Error"]CodeMessage 旁边找,一无所获,然后断定这个参数没生效。
  • 项是以 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 的 bug。

为什么表达式里每个名字都做了别名

代码里写的是 #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 agent。

30 天免费试用,无需信用卡 — 之后为无时间限制的免费版。