中階閱讀時間 2 分鐘

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_existslist_append
  • ADD 做一次原子數字遞增或集合聯集,在一趟往返中完成, 不需要先讀取。
  • REMOVE 直接刪除一個屬性(或依索引刪除單一清單元素)。
  • DELETE 從一個集合移除特定成員 — 且只從集合。

從 SQL 過來,陷阱是凡事都伸手去拿 SETADDDELETE 之所以存在,是因為對計數器或集合做讀-改-寫,是一場你在 並發下會輸的競爭。

依你在改什麼挑子句

一個 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,而非 REMOVEREMOVE 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 + :priceSET 內的算術作用於 既有的值。它不像 ADD 那樣對競爭安全是原子的,但它在 伺服器端讀目前的總額,而非繞著你的程式碼來回它。

要避開的陷阱

  • SET 一個你先讀過的計數器。ADD — 讀-改-寫在 並發下會遺失更新。這是最常見的購物車/庫存 bug。
  • 對一個缺少的清單用 list_append 把目標包進 if_not_exists,否則 第一次寫入會失敗。
  • 搞混 REMOVEDELETE REMOVE 丟掉屬性;DELETE 從一個集合減去成員。把它們混用會刪掉超過你本意的東西。
  • 忘了 UpdateItem 是 upsert。 如果 key 不存在,它會建立 那個 item。當你的意思是「只更新」時,用一個 ConditionExpressionattribute_exists(CartPK))。

要建模這些 expression 所對之執行的 key,請見 single-table design;要決定你會如何讀回 購物車,請見 query vs scan

expression builder 中建立並複製這些之中的任何一個,然後 試試 DynoTable 對你自己的表執行它們,並看著 item 即時變化。

已更新