DynamoDB Update Expression
update expression 告訴 UpdateItem 如何變更單一 item:要寫入、
遞增、刪除哪些屬性,或把哪些折進一個集合。沒有 UPDATE … SET … WHERE — 你用 key 指名那個 item,並用四個子句關鍵字描述
變更。
DynamoDB update expression 如何運作?
DynamoDB update expression 告訴 UpdateItem 如何用四個子句變更一個 item。SET 寫入或覆寫一個屬性。ADD 原子地遞增數字或聯集進一個集合。REMOVE 刪除一個屬性或清單中的單一元素。DELETE 從集合移除特定成員。一次呼叫可同時帶上全部四個子句。
SET寫入或覆寫一個屬性 — 純量、文件,以及函式 慣用法if_not_exists與list_append。ADD做一次原子數字遞增或集合聯集,在一趟往返中完成, 不需要先讀取。REMOVE直接刪除一個屬性(或依索引刪除單一清單元素)。DELETE從一個集合移除特定成員 — 且只從集合。
從 SQL 過來,陷阱是凡事都伸手去拿 SET。ADD 與
DELETE 之所以存在,是因為對計數器或集合做讀-改-寫,是一場你在
並發下會輸的競爭。
依你在改什麼挑子句
一個 UpdateItem 呼叫能一次帶上全部四個子句,順序是
SET … REMOVE … ADD … DELETE。每個關鍵字最多出現一次,並接一個
以逗號分隔的動作清單。
| 子句 | 作用於 | 用它來 |
|---|---|---|
SET | 任何屬性 | 寫入/覆寫一個值或文件欄位 |
ADD | 僅 Number 或 Set | 原子地遞增,或聯集進一個集合 |
REMOVE | 任何屬性或清單元素 | 刪除一個屬性;丟掉一個清單索引 |
DELETE | 僅 Set | 從一個集合移除特定成員 |
對字串用 ADD、對純量用 DELETE 是驗證錯誤,而非無操作 —
DynamoDB 拒絕整個呼叫。依據
AWS update-expression 參考,
ADD 限於數字與集合,DELETE 限於集合。
演練範例:一個購物車
每個購物車一個 item,以 CartPK = "CART#c-9f21" 與 CartSK = "SUMMARY" 為 key。
它追蹤一個累計的 OrderTotal、一個 LineItems 清單、一個 PromoCodes 字串集合,
以及一個 ItemCount。
SET — 寫入純量與文件
SET 覆寫原本在那的任何東西。在同一個呼叫中把一個 line item 加進清單、
並把總額加上去:
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(一個空清單 [])。那讓第一次新增與之後每次
新增都用同一個 expression — 這些慣用法存在的一個真正理由。
ADD — 原子地遞增計數
要把 ItemCount 加上去,不要 讀它、在你的程式碼裡加一、再 SET 回去。
那是一個更新遺失競爭:兩個並發的新增都讀到 3、都寫 4,於是
你掉了一個。ADD 在伺服器端做算術:
ADD ItemCount :one
搭配 :one = 1,這是一個原子計數器。並發呼叫在那個
item 上序列化,所以兩次新增落地為 +2。傳一個負數來遞減。如果
ItemCount 缺席,ADD 先把它當成 0 — 所以你絕不需要種下
那個計數器。
你可以在
DynamoDB expression builder 中建立這個確切的
expression — 名稱、型別化的值,以及 marshalled 的請求 — 而不必
手動逸出任何一個 #name 或 :value 佔位符。
REMOVE — 丟掉一個屬性或一個 line item
REMOVE 是你如何完全刪除一個屬性的方式(沒有「設成 null」 —
那只是寫一個 NULL 型別)。在一個呼叫中清掉一個套用的折扣、並丟掉第三個
line item:
REMOVE AppliedDiscount, LineItems[2]
LineItems[2] 移除索引 2 的元素,並 把它之後的一切往下移 —
索引 3 變成 2,依此類推。如果你在一個 expression 中 REMOVE 兩個索引,
兩者都對 原始 清單評估,所以一起移除 [2]
與 [3] 會如你所料地丟掉第三與第四個元素。
DELETE — 移除集合成員
PromoCodes 是一個字串集合,所以一位客戶撤下一個代碼用 DELETE,而非
REMOVE。REMOVE PromoCodes 會把整個集合炸掉;DELETE 減去
指名的成員:
DELETE PromoCodes :pulled
搭配 :pulled = 集合 {"SAVE10"},只有那個成員會走。兩條規則在
這裡咬人:一個集合永遠不能為空,所以刪除最後一個成員會直接
移除 PromoCodes 屬性;而那個值必須是與屬性相符的集合型別 —
一個光禿禿的字串是型別錯誤。
把它組起來
一個「加 item、套用一個促銷、把計數加上去」的更新,是橫跨三個 子句的一個呼叫:
SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem),
OrderTotal = OrderTotal + :price
ADD ItemCount :one
DELETE PromoCodes :expiredCode
注意 OrderTotal = OrderTotal + :price — SET 內的算術作用於
既有的值。它不像 ADD 那樣對競爭安全是原子的,但它在
伺服器端讀目前的總額,而非繞著你的程式碼來回它。
要避開的陷阱
SET一個你先讀過的計數器。 用ADD— 讀-改-寫在 並發下會遺失更新。這是最常見的購物車/庫存 bug。- 對一個缺少的清單用
list_append。 把目標包進if_not_exists,否則 第一次寫入會失敗。 - 搞混
REMOVE與DELETE。REMOVE丟掉屬性;DELETE從一個集合減去成員。把它們混用會刪掉超過你本意的東西。 - 忘了
UpdateItem是 upsert。 如果 key 不存在,它會建立 那個 item。當你的意思是「只更新」時,用一個ConditionExpression(attribute_exists(CartPK))。
要建模這些 expression 所對之執行的 key,請見 single-table design;要決定你會如何讀回 購物車,請見 query vs scan。
在 expression builder 中建立並複製這些之中的任何一個,然後 試試 DynoTable 對你自己的表執行它們,並看著 item 即時變化。