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

PutItem 写入一整个项,并替换掉主键相同的既有项(基于项的操作讲了它和 UpdateItem 的区别)。v3 客户端直接发送 DynamoDB JSON,所以 Item 里装的是 { S: … } / { N: … } 这样的值,而不是普通的 JavaScript 值。

代码

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

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

const command = new PutItemCommand({
  TableName: 'Music',
  Item: {
    Artist: {S: 'Arturo Sandoval'},
    SongTitle: {S: 'Cubano Chant'},
    AlbumTitle: {S: 'Danzon'},
    Year: {N: '1994'},
    Awards: {N: '0'}
  },
  ConditionExpression: 'attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)',
  ExpressionAttributeNames: {
    '#cond0': 'Artist',
    '#cond1': 'SongTitle'
  }
});

try {
  await client.send(command);
  console.log('Song written');
} catch (err) {
  if (err.name === 'ConditionalCheckFailedException') {
    console.log('A song with that key already exists — not overwritten');
  } else {
    throw err;
  }
}

说明

err.name 是正确的判断依据,而它不是错误对象上唯一的东西。把上面那次条件失败捕获下来并打印出这个对象,得到的是:

err.name                     ConditionalCheckFailedException
err instanceof Error         true
err.message                  The conditional request failed
err.$metadata.httpStatusCode 400

每个 v3 错误都带着 $metadata,里面有状态码、请求 id 和尝试次数,这正是你想写进日志的东西。err.name 在各个模块化包之间都是稳定的;instanceof ConditionalCheckFailedException 也能用,但那会把类作为值引入,于是打包器会把它留下。

错误可以把挡住这次写入的那个项交给你。给命令加上 ReturnValuesOnConditionCheckFailure: 'ALL_OLD'err.Item 就会带着内容回来:上面那次运行里是五个属性,其中 Year{"N":"1994"}。大多数"只创建"的处理逻辑会在失败之后再做一次 GetItem 去看看那里原本是什么。那一次往返是可以省掉的。(ReturnValues: 'ALL_OLD' 是它在成功路径上的表亲;ReturnValues 讲了其余的部分。)

marshall() 拒绝的输入比你以为的多。把本页这种带类型的 Item 换成 DynamoDBDocumentClient 加普通对象,是通常的下一步,而 @aws-sdk/util-dynamodb 默认就很严格。三个真实的抛错,原样照录:

{Genre: undefined}   Pass options.removeUndefinedValues=true to remove undefined values from map/array/set.
{tags: new Set()}    Pass a non-empty set, or options.convertEmptyValues=true.
{n: 9007199254740993} Number 9007199254740992 is greater than Number.MAX_SAFE_INTEGER. Use NumberValue from @aws-sdk/lib-dynamodb.

第一个才是会进到生产环境的那个:一个可选字段是 undefined 而不是干脆不存在,就会在编组时抛错,而在 DynamoDBDocumentClient.from(client, {marshallOptions}) 里设 removeUndefinedValues: true 是标准修法。

再读一遍第三行。字面量是 9007199254740993,消息里引用的却是 9007199254740992。JavaScript 在 SDK 看到这个值之前就已经把它四舍五入了,所以 SDK 报的是它收到的东西。这正是 DynamoDB 用字符串传输 N 的全部理由:它能保 38 位精度,而一个 JS number 只有 15 到 17 位。真正属于标识符的东西应该放在 S 里,真正属于小数的东西应该放进 NumberValue,或者你自己格式化的字符串。

ConditionExpression 即使被拒绝也要花写容量。AWS 的原话是:"if the expression evaluates to false, DynamoDB still consumes write capacity units from the table"(2026-07-28 获取)。一个紧凑的"只创建"重试循环是按尝试次数计费的。作为参照,成功写入一个约 15 KB 的项报告的是 "CapacityUnits": 15;写入按每 1 KB 向上取整,而不是读取用的 4 KB。

这些别名是承重的#cond0/#cond1 通过 ExpressionAttributeNames 解析成 Artist/SongTitle。直接内联名字一直能用,直到其中一个撞上保留字,然后表达式就会在一个你根本没碰过的属性上失败。

用可视化的方式来做

上面那些编组规则,最容易的检查方式是把两种形式并排看一遍。免费的 DynamoDB JSON 转换器能把普通 JSON 变成带类型的 { S: … } 形式再变回来,于是你可以在发送之前确认 marshall() 会产出什么。

要在你自己的表上写入和编辑项——每个属性一个表单、有类型选择器、还能把结果作为 SDK v3 代码复制出来——请下载 DynoTable

相关指南

参考资料

2026-07-28 在 Node v24.18.0 上、使用 @aws-sdk/client-dynamodb 3.1095.0 与 @aws-sdk/util-dynamodb 3.996.7,针对 9000 端口上的 DynamoDB Local(amazon/dynamodb-local)复现。上面的错误字符串、对象结构和容量读数都是捕获到的输出,原样照录。

无需控制台即可使用 DynamoDB

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

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