DynamoDB 更新表达式:SET、REMOVE、ADD、DELETE(含示例)
更新表达式告诉 UpdateItem 如何变更单个项目:要写入、递增、删除或折叠进集合的是哪些。这里没有多行的 UPDATE … SET … WHERE——你用完整的键点名一个项目,再用四个子句关键字描述变更。
DynamoDB 更新表达式如何工作?
DynamoDB 更新表达式用四个子句告诉 UpdateItem 如何变更某一个项目。SET 写入或覆盖一个。ADD 原子地递增一个数字,或并入一个集合。REMOVE 删除一个属性或一个列表元素。DELETE 从一个集合中移除特定成员。一次调用可以同时携带全部四个。
SET写入或覆盖一个属性——标量、文档,以及函数惯用法if_not_exists和list_append。ADD在一次往返中完成原子的数字递增或集合并集,无需先读。REMOVE彻底删除一个属性(或按下标删除单个列表元素)。DELETE从一个集合中移除特定成员——而且只作用于集合。
从 SQL 过来时,陷阱是什么都拿 SET 去做。ADD 和 DELETE 之所以存在,是因为对计数器或集合做读-改-写,在并发下是一场你必输的竞态。
按你要改动的内容来选子句
一次 UpdateItem 调用可以同时携带全部四个子句,顺序任意。每个关键字最多出现一次,并接受一个逗号分隔的动作列表。
| 子句 | 作用于 | 用它来做 |
|---|---|---|
SET | 任意属性 | 写入/覆盖一个值或文档字段 |
ADD | 仅数字或集合 | 原子递增,或并入一个集合 |
REMOVE | 任意属性或列表元素 | 删除一个属性;去掉一个列表下标 |
DELETE | 仅集合 | 从一个集合中移除特定成员 |
对字符串用 ADD、对标量用 DELETE 是验证错误,而不是空操作——DynamoDB 会拒绝整个调用。按照 AWS 更新表达式参考,ADD 仅限于数字和集合,DELETE 仅限于集合。
实战示例:一个购物车
每个购物车一个项目,键为 CartPK = "CART#c-9f21" 和 CartSK = "SUMMARY"。它跟踪一个累计的 OrderTotal、一个 LineItems 列表、一个 PromoCodes 字符串集合,以及一个 ItemCount。
SET——写入标量和文档
SET 会覆盖原有内容。在同一次调用里往列表添加一个行项目并累加总额:
SET OrderTotal = :total,
LineItems = list_append(LineItems, :newItem),
UpdatedAt = :now
list_append(LineItems, :newItem) 追加到尾部;把参数调个位置——list_append(:newItem, LineItems)——就是前插。参数的顺序就是拼接的顺序,仅此而已。
那第一个调用里藏着一个坑:如果购物车是全新的,LineItems 还不存在,而在一个缺失的属性上做 list_append 会失败。用 if_not_exists 守卫它:
SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem)
if_not_exists(LineItems, :empty) 在列表存在时返回当前列表,否则返回后备值 :empty(一个空列表 [])。这就让第一次添加和之后每一次添加都使用同一个表达式——这些惯用法存在的真正理由。
ADD——原子地递增计数
要给 ItemCount 加一,不要先把它读出来、在你的代码里加一、再 SET 回去。那是一场丢失更新的竞态:两个并发的添加都读到 3,都写入 4,于是你丢了一个。ADD 在服务端完成算术:
ADD ItemCount :one
当 :one = 1 时,这就是一个原子计数器。并发调用会在该项目上串行化,所以两次添加最终落成 +2。传一个负数就是递减。如果 ItemCount 不存在,ADD 会先把它当作 0——所以你永远不必给计数器播种。
在 us-east-1 的按需模式下,每一次 ADD 都按写入后项大小每 KB 1 WCU 计费
(向上取整)。一个 1 KB 的购物车摘要项,带一个 ADD ItemCount :one,花 1 WCU ——
同一行如果改用读-改-写的 SET,WCU 一样,却会在并发下丢更新。用
项大小计算器给这个项定尺寸。
你可以在 DynamoDB 表达式构建器里构建出这个一模一样的表达式——名称、带类型的值,以及编组后的请求——完全不必手工转义任何一个 #name 或 :value 占位符。
编辑下面这个预设——一个 SET 加一个原子 ADD——然后看着 UpdateExpression 随你的操作实时重建:
REMOVE——删掉一个属性或一个行项目
REMOVE 是你彻底删除一个属性的方式(并没有「把它设为 null」这种事——那只会写入一个 NULL 类型)。在一次调用里清掉已应用的折扣、并去掉第三个行项目:
REMOVE AppliedDiscount, LineItems[2]
LineItems[2] 会移除下标 2 处的元素,并把它之后的一切往前挪——下标 3 变成 2,以此类推。如果你在一个表达式里 REMOVE 两个下标,两者都是针对原始列表求值的,所以把 [2] 和 [3] 一起移除,会如你所料地删掉第三和第四个元素。
DELETE——移除集合成员
PromoCodes 是一个字符串集合,所以顾客撤掉一个促销码时用的是 DELETE,而不是 REMOVE。REMOVE PromoCodes 会把整个集合抹掉;DELETE 只减去指定的成员:
DELETE PromoCodes :pulled
当 :pulled = 集合 {"SAVE10"} 时,只有那个成员会被删掉。这里有两条规则会咬人:集合永远不能为空,所以删掉最后一个成员会把 PromoCodes 属性整个移除;而且值必须是与该属性匹配的集合类型——一个裸字符串是类型错误。
当 :pulled = 集合 {"SAVE10"} 时,只有该成员离开。两条规则咬合这里:集合永远不能为空,因此删除最后一个成员会删除
PromoCodes 完全属性;并且该值必须是与属性——裸字符串是类型错误。
把它们组合起来
一次「加入商品、应用促销、累加计数」的更新,就是横跨三个子句的一次调用:
SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem),
OrderTotal = OrderTotal + :price
ADD ItemCount :one
DELETE PromoCodes :expiredCode
注意 OrderTotal = OrderTotal + :price——SET 内部的算术是作用于现有值的。它和 ADD 一样原子、一样对竞态安全:DynamoDB 在服务端针对当前值求值 OrderTotal + :price,所以并发调用会在该项目上串行化,而不是绕经你的代码往返。
要避开的陷阱
- 对一个你先读过的计数器用
SET。 请用ADD——读-改-写在并发下会丢失更新。这是最常见的购物车/库存 bug。 - 在一个缺失的列表上用
list_append。 把目标裹进if_not_exists,否则第一次写入就会失败。 - 混淆
REMOVE和DELETE。REMOVE丢掉整个属性;DELETE从一个集合里减去成员。混用它们会删掉比你本意更多的东西。 - 忘了
UpdateItem是一个 upsert。 如果键不存在,它会创建该项目。当你的本意是「只更新」时,请用一个ConditionExpression(attribute_exists(CartPK))。
关于给这些表达式所作用的键建模,见单表设计;关于如何决定你要怎样把购物车读回来,见 query vs scan。
在表达式构建器里构建并复制以上任意一个,然后试用 DynoTable对着你自己的表运行它们,看着项目实时变化。