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 更新運算式參考,@@P25@@ 僅限於數字和集合,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對著你自己的表執行它們,看著項目實時變化。