DynamoDB Condition Expression
condition expression 是 DynamoDB 在提交你的寫入 之前,對既有 item
評估的一個述詞。如果述詞為假,寫入被拒絕,什麼也不會
改變。它是 DynamoDB 在寫入上最接近 WHERE 子句的東西 — 也是
強制一個不變式唯一安全的方式。
DynamoDB condition expression 如何運作?
condition expression 是 DynamoDB 在提交寫入前,於伺服器端對目前 item 評估的一個述詞。如果為真,寫入照常進行;如果為假,寫入會被 ConditionalCheckFailedException 拒絕,什麼也不會改變。它把檢查與變更折進單一原子操作,因此並發的呼叫者無法以陳舊的讀取造成競爭。
- 它是守衛,不是過濾器。
ConditionExpression在伺服器端對 目前的 item 執行;假的結果會以ConditionalCheckFailedException讓寫入失敗。 - 它取代讀-然後-寫。 沒有
SELECT再UPDATE的往返 — 檢查 與變更是一個原子操作,所以兩個呼叫者無法競爭。 - 拒絕免費,執行不免費。 一次失敗的條件式寫入仍會 消耗寫入容量。那個保證的成本跟它擋下的寫入相同。
從 SQL 過來,你會讀那一列、在應用程式碼裡檢查它,然後更新。在 DynamoDB 中,讀與寫之間那道縫隙,是一個等著某個並發呼叫者的 資料毀損 bug。condition expression 把那道縫隙關上。
它們適用於哪裡
你把一個 ConditionExpression 附加到 PutItem、UpdateItem、DeleteItem,以及
TransactWriteItems 內的每個動作。它 不是 Query 或 Scan 的一部分
— 那些用 FilterExpression,那是讀取路徑上不同的東西。
那個區別會絆倒人,所以要精確:
ConditionExpression | FilterExpression | |
|---|---|---|
| 路徑 | 寫入(Put/Update/Delete) | 讀取(Query/Scan) |
| 失敗時的效果 | 拒絕整個寫入 | 把 item 從結果中丟掉 |
| 看到的 | 目前的 item,寫入前 | 每個候選 item,讀取後 |
| 成本 | 失敗的寫入仍計費 | 被過濾的 item 仍就讀取計費 |
兩者都在伺服器端執行。差別在於「假」的作為:一個條件中止一次 變更;一個過濾只是隱藏一列你已付費讀取的資料。 (AWS:Condition Expressions)
你實際會用到的函式
condition 語言很小。主力:
attribute_exists(path)/attribute_not_exists(path)— 這個屬性在 item 上 存在嗎?「只在不存在時建立」/「只在存在時更新」的經典慣用法。- 比較運算子 —
=、<>、<、<=、>、>=— 對一個值或另一個 屬性。 attribute_type、begins_with、contains、size— 型別與字串/集合檢查。BETWEEN … AND …、IN (…)— 範圍與成員檢查。AND、OR、NOT、括號 — 把上述組合起來。
對 partition key 用 attribute_not_exists,是讓 PutItem 表現得像一個
不會覆蓋既有 item 的插入的標準方式 — DynamoDB
沒有獨立的「insert」操作,所以那個條件 就是 插入語意。
(AWS:Comparison Operator and Function Reference)
演練範例:守衛一個帳本防止透支
拿一個銀行帳本。每個帳戶是一個 item:
PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents = 0不變式:一筆扣款絕不能把可用餘額推到零以下,且你 絕不能扣一個不存在的帳戶。兩條規則,都能在寫入本身內 強制。
錯誤的方式(地雷)
GetItem ACCT#a7f3 / BALANCE → clearedCents = 50000
if (50000 >= 30000) ... ← 應用程式端檢查
UpdateItem SET clearedCents = 20000
在 GetItem 與 UpdateItem 之間,第二筆扣款可以讀到同一個
50000、通過它自己的檢查、也寫入。兩者都成功;帳戶變
負。這是一個讀-改-寫競爭,再多應用程式端驗證都
修不了它 — 檢查與寫入是分開的操作。
正確的方式
把檢查折進寫入。扣款 30000 cents,條件是帳戶 存在 且 持有足夠:
UpdateItem ACCT#a7f3 / BALANCE
SET clearedCents = clearedCents - :amt
ConditionExpression:
attribute_exists(PK) AND clearedCents >= :amt搭配 :amt = 30000。如果餘額太低,或 item 從未被建立,
DynamoDB 以 ConditionalCheckFailedException 拒絕寫入,而餘額
不被觸碰。並發的那筆扣款,要嘛看到原始餘額並被
對它檢查,要嘛看到更新後的 — 絕不是它據以動作的陳舊讀取。
你可以用 DynamoDB expression builder
建立並複製確切的 expression — 名稱、值,全部 — 而不必
手動組裝 ExpressionAttributeValues map。
在 DynoTable 中檢視那個守衛
當一次條件式寫入失敗時,你想看到 item 的真實狀態,而非
猜測它。把帳戶 item 拉出來,直接讀 clearedCents。

讀懂那個拒絕,別盲目重試
ConditionalCheckFailedException 不是暫時性錯誤 — 重試同一個
寫入什麼也不會改變。它意味著一條業務規則觸發了:餘額不足、
重複建立、版本陳舊。把它呈現為一個領域結果,而非一個基礎設施
小毛病。
兩件事能讓失敗可除錯:
ReturnValuesOnConditionCheckFailure: ALL_OLD— DynamoDB 連同失敗 一併回傳目前的 item,這樣你不必第二次讀取就能顯示「餘額是 20000,你要求 30000」。 (AWS:Working with Items)- 區分兩個失敗原因。
attribute_exists(PK) AND clearedCents >= :amt把「沒有帳戶」與「沒有資金」合併成一個 例外。如果呼叫者需要分辨它們,拆成兩次寫入,或檢視 回傳的 item。
樂觀鎖定是同一個招式
版本號模式只是戴著不同帽子的 condition expression。
存一個 version 屬性;每次寫入都斷言你讀到的版本,並
把它加一:
UpdateItem ACCT#a7f3 / BALANCE
SET clearedCents = :new, version = :next
ConditionExpression: version = :seen如果另一個寫入者先動了,version = :seen 為假,寫入被拒絕,
而你重讀並重試。這就是 DynamoDB 在沒有鎖的情況下做並發
控制的方式 — 斷言你所見、若它動了就失敗。(AWS:Optimistic Locking with
Version Number)
陷阱與下一步
- 與保留字衝突的名稱。
status、size、name,以及約 570 個其他字是保留的。用ExpressionAttributeNames(#s = status) 替它們取別名,否則你的 expression 會悄悄無法解析。 - 一個條件無法參照另一個 item。 它只看到正在被
寫入的 item。跨 item 的不變式需要
TransactWriteItems搭配每個動作的ConditionExpression,或對一個哨兵 item 的ConditionCheck。 - 失敗的寫入仍花 WCU。 一個 90% 時間都拒絕的守衛,仍會就那些 拒絕計費。便宜的保險,但不免費。
要建模這些守衛所對之執行的 key,請見 single-table design 與 Query vs Scan。當你準備好對真實資料發出條件式 寫入時,下載 DynoTable 並對你自己的表執行它們。


