Node.js 中的 DynamoDB 条件写入(AWS SDK v3)

在 AWS SDK v3 里,条件写入有意思的地方不是 ConditionExpression——它到哪儿都一个样,DynamoDB 条件表达式里讲过了。有意思的是失败路径:如果你开口要了,v3 会把落败的那个项目挂在抛出的错误上交给你;你没要,它就什么都不给。

代码

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

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

// Update the item only if nobody changed it since we read version 7.
const command = new UpdateItemCommand({
  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'
});

try {
  await client.send(command);
  console.log('Updated to version 8');
} catch (err) {
  if (err.name === 'ConditionalCheckFailedException') {
    // With ReturnValuesOnConditionCheckFailure: 'ALL_OLD', the current item
    // rides back on the exception — no extra read to see what beat you.
    console.log('Lost the race — item is now:', err.Item);
  } else {
    throw err;
  }
}

说明

  • 条件检查失败是抛出的错误,不是一个状态字段。v3 会 reject 这个 promise,所以写入成功的路径和竞争落败的路径是两条不同的分支。err.name === 'ConditionalCheckFailedException' 是判别式;其他任何东西都必须重新抛出,这也是代码里那个 else 的用处。把整个 catch 吞掉,你就把一次限流悄悄变成了什么都没做。
  • ReturnValuesOnConditionCheckFailure 是唯一能看到谁赢了你的办法。没有它,错误只带着消息、别的什么都没有,你又得回去做一次本来不需要的 GetItem。API 参考把它的有效值定为 ALL_OLD | NONE,并确认它不消耗读取容量。
  • err.Item 是一个原始的 AttributeValue 映射,形状和你发过去的 Key 一样,不是普通的 JavaScript 值。在拿 Version 跟数字比较之前,先用 @aws-sdk/util-dynamodbunmarshall 过一道,否则你比较的对象是 {N: '9'}
  • 失败的写入照样要计费。开发者指南写得很明白:条件求值为 false 仍然会消耗写入容量,按新旧项目中较大的那个来算。热键上的重试循环在账单上是实打实的一行,所以要给尝试次数封顶。
  • 代码里每个名字都做了别名#versionVersion#cond0Artist),因为生成它的表达式构建器无条件地做别名。在这里这比必要的更重,但永远不会错——这就是它做的取舍。

从异常里读出落败者的那份副本

把存储的 Version 设为 9,再运行这段期望值为 7 的代码。DynamoDB Local 3.3.0 会抛出异常,捕获到的错误带着:

err.name     ConditionalCheckFailedException
err.message  The conditional request failed
err.$metadata.httpStatusCode  400
err.Item     {
               Artist:     { S: 'Arturo Sandoval' },
               Year:       { N: '1994' },
               Version:    { N: '9' },
               SongTitle:  { S: 'Cubano Chant' },
               AlbumTitle: { S: 'Danzon' }
             }

那个 Version: 9 才是重点所在。重试可以把 :expectedVersion 设成 9 直接再走一遍更新,既不用额外读一次,也不会留下一个让第三个写入者从你的 GetItem 和重试之间钻进来的窗口。

ReturnValuesOnConditionCheckFailure 从同一个命令里删掉再跑一次。一样的 name、一样的 message、一样的 400,而 err.Itemundefined。没有任何东西提醒你:这个参数是可选的,缺了它不算错误,而读 err.Item 的代码只会在生产里开始打印 undefined

还要注意,这里的 400 并不代表请求格式有问题。ValidationExceptionConditionalCheckFailedException 共用这个状态码,而其中只有一个是 bug——这正是分支要判 err.name、绝不判状态码的原因。

想针对自己的数据看着一个条件成功和失败,并且表达式是替你写好而不是敲出来的,就下载 DynoTable

相关示例

参考资料

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

无需控制台即可使用 DynamoDB

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

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