Python(boto3)中的 DynamoDB UpdateItem

boto3 为这次调用给了你两个 client,而它们对「数字是什么」意见不一致。下面的低层 client 收发 DynamoDB JSON,其中每个数字都是带引号的字符串。resource("dynamodb").Table(...) 接受原生 Python 对象,彻底拒绝 float,并把数字以 decimal.Decimal 交还给你。挑哪一个,才是本页真正要做的决定。

代码

import boto3

client = boto3.client("dynamodb")

response = client.update_item(
    TableName="Music",
    Key={"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
    UpdateExpression="SET #upd0 = :updValue0, #upd1 = :updValue1 ADD #upd2 :updValue2",
    ExpressionAttributeNames={"#upd0": "Genre", "#upd1": "Year", "#upd2": "Awards"},
    ExpressionAttributeValues={":updValue0": {"S": "Latin Jazz"}, ":updValue1": {"N": "1994"}, ":updValue2": {"N": "1"}},
    ReturnValues="ALL_NEW",
)

print(response["Attributes"])  # the item after the update

说明

  • 子句语法不归 boto3 管UpdateExpression 是它原样转发的一个不透明字符串;只有 DynamoDB 会解析它,所以写错的代价是一次往返。这里的 ADD 是那种能消掉「读-改-写」竞争的原子自增,在 ConditionExpression 里加 attribute_exists(Artist) 会把 upsert 变成只更新,其余内容在更新表达式里。
  • 响应正好有两个顶层键AttributesResponseMetadata。没有状态字段要检查,也没有影响行数。只要调用返回了,它就成功了;ResponseMetadata 带着你想写进日志的 RequestIdHTTPStatusCode
  • ReturnValues="UPDATED_NEW" 是省钱的选项。它只返回表达式碰过的属性,在一个大项目上,这就是「读一个计数器」和「把整条记录搬回来」的区别。
  • 错误以 botocore.exceptions.ClientError 的形式到达,你按 e.response["Error"]["Code"] 分支。少一个别名会产生 ValidationException,消息是 Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: Year。带类型的子类确实存在,但只作为 botocore 在 client 实例上生成的属性(client.exceptions.ConditionalCheckFailedException),永远不是可导入的符号,所以一个作用域里没有 client 的辅助函数只能用错误码字符串。

Decimal 还是 DynamoDB JSON,二选一

资源 API 在请求被构建之前就拒绝 float,而且消息把它想要什么说得清清楚楚:

TypeError: Float types are not supported. Use Decimal types instead.

那是 boto3 自己的类型检查,不是 DynamoDB 的。通过资源 API 存入 Decimal("4.5"),再用两个 client 分别把同一个属性读回来,你会得到:

resource Rating: Decimal('4.5') Awards: Decimal('2')
client Rating: {'N': '4.5'} Awards: {'N': '2'}

两者都没错;它们是不同的契约。Decimal 保住了 DynamoDB 实际存储的精度,并逼你去认真对待算术,代价是 Decimal("1") * 2 会出现在期待 int 的代码里。低层 client 交给你字符串,解析留给你自己,上面那个代码片段就是这么做的。

由此得出的规则是:不要在同一条代码路径里混用它们。通过 Table.put_item 写入、又通过 client.get_item 读出的项目,形状是不同的,而这个 bug 会出现在你测得较少的那个分支里。

关于 TTL 属性的一点说明

Python 代码库里最常见的数值 SET 是 TTL:SET expires_at = :t,配上一个 Unix 纪元时间。DynamoDB 把那个属性读作。写成 int(time.time() * 1000) 的话,值是 1785269450912,当成秒解释就落在公元 58542 年,于是项目永远不会被删除,也没有任何东西报警。DynamoDB TTL 转换器会用两种单位把一个纪元时间读回来,并告诉你写进去的是哪一种。之后要从真实的表里把存进去的值读回来,请下载 DynoTable

相关指南

参考资料

最后核实于 2026-07-28,依据上方链接的 AWS 官方文档。

无需控制台即可使用 DynamoDB

一款快速的 DynamoDB 桌面客户端,可运行 DynamoDB 无法执行的真正 SQL——JOINs、GROUP BY、聚合——并支持可视化编辑和运行在你自己的 Bedrock 密钥上的 AI agent。

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