Node.js 中的 DynamoDB UpdateItem(AWS SDK v3)

底层的 v3 client 在两个方向上说的都是 DynamoDB JSON,这意味着你发出去的每一个数字和拿回来的每一个数字都是字符串。这不是瑕疵;这是让一个 38 位的 DynamoDB 数字在一门唯一数字类型是 double 的语言里活下来的唯一办法。这也正是 bug 藏身之处。

代码

import {DynamoDBClient, UpdateItemCommand} from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({region: 'us-east-1'});

const command = new UpdateItemCommand({
  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'
});

const response = await client.send(command);
console.log(response.Attributes); // the item after the update

针对一个原本既没有 Genre 也没有 Awards 的项目,response.Attributes 回来时是:

{"Artist":{"S":"Arturo Sandoval"},"Awards":{"N":"1"},"Genre":{"S":"Latin Jazz"},"Year":{"N":"1994"},"SongTitle":{"S":"Cubano Chant"}}

typeof response.Attributes.Awards.N"string",所以 response.Attributes.Awards.N + 1 求值为 "11"。没有东西抛错,没有东西警告,而这个错误的数字会进到你的下一次写入里。在边界处解析:Number(response.Attributes.Awards.N)

说明

  • 表达式就是一个普通字符串,而 v3 不会检查它UpdateItemCommand 校验的是输入对象的形状,从不校验 UpdateExpression 内部的语法,所以一个笔误换来的是一次往返和一个 400。语法在更新表达式里;ADD #upd2 :updValue2 是原子自增,而加上 ConditionExpression: 'attribute_exists(Artist)' 会让这次调用变成只更新而不是 upsert。

  • ReturnValues: 'UPDATED_NEW' 通常才是你想要的那个。同样的更新只返回 {"Awards":{"N":"2"}},别的什么都没有。ALL_NEW 每次调用都把整个项目运回来,在一个大项目上,那是你为读一个计数器而付出的带宽。

  • $metadata 是 v3 的带外通道{"httpStatusCode":200,"requestId":"...","attempts":1,"totalRetryDelay":0}attempts 是对"这次重试了吗"的诚实回答,而当你在推断一次非幂等写入是不是跑了两遍时,这一点很要紧。

  • ValidationException 不是一个你能 catch 的类,只是一个你能比较的 name。缺一个别名回来的是 err.name === 'ValidationException'err.message 被设为 Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: Year

  • 文档 client 是另一笔交易@aws-sdk/lib-dynamodb 接收原生 JS 值并反序列化响应,代价是丢掉那份字符串安全。来自 @aws-sdk/util-dynamodbmarshall({awards: 9007199254740993}) 会直接拒绝:

    Number 9007199254740992 is greater than Number.MAX_SAFE_INTEGER. Use NumberValue from @aws-sdk/lib-dynamodb.

    仔细看那条消息里的数字。它结尾是 2,而不是字面量里写的那个 3:JavaScript 在 SDK 看到它之前就已经把它舍入过了。本例里的底层 client 不可能有这个问题,因为 {N: '9007199254740993'} 一路到线上都是文本。

条件失败会交给你什么

在输入里加上 ReturnValuesOnConditionCheckFailure: 'ALL_OLD',抛出的错误就会带着赢了你的那个项目:

name: ConditionalCheckFailedException | message: "The conditional request failed" | http: 400
err.Item: {"Artist":{"S":"Arturo Sandoval"},"Awards":{"N":"2"},"Year":{"N":"1994"},"SongTitle":{"S":"Cubano Chant"},"Genre":{"S":"Latin Jazz"}}

无论是哪个 client 抛出的,err.Item 都是原始的 DynamoDB JSON,而且它是免费的。没有它,想搞清楚一次乐观并发更新为什么失败,诚实的办法是再补一次 GetItem——它要花一次读取,而且可能一拿到就又过时了。

DynamoDB JSON 转换器能把那份负载变成一个普通 JS 对象再变回去,这是从一个真实项目造出测试夹具最快的办法。至于一开始怎么从实时表里把那个项目捞出来,下载 DynoTable

相关指南

参考资料

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

无需控制台即可使用 DynamoDB

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

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