DynamoDB の更新式
更新式は、UpdateItem に単一のアイテムをどう変更するか — どの属性を書き込み、
インクリメントし、削除し、あるいはセットに畳み込むか — を伝えます。UPDATE … SET … WHERE はありません — アイテムをキーで名指しし、4つの句キーワードで変更を記述します。
DynamoDB の更新式はどのように機能しますか?
DynamoDB の更新式は、UpdateItem に4つの句を使って1つのアイテムをどう変更するかを伝えます。SET は属性を書き込むか上書きします。ADD は数値をアトミックにインクリメントするか、セットに和集合します。REMOVE は属性またはリストの1要素を削除します。DELETE はセットから特定のメンバーを取り除きます。1回の呼び出しで4つすべてを同時に指定できます。
SETは属性を書き込むか上書きします — スカラー、ドキュメント、そして関数イディオムif_not_existsとlist_append。ADDは、読み取りを先行させずに、1回のラウンドトリップでアトミックな数値の インクリメントかセットの和集合を行います。REMOVEは属性をまるごと削除します(あるいはリストの単一要素をインデックスで)。DELETEはセットから特定のメンバーを取り除きます — セットからのみ。
SQL から来ると、罠はすべてに SET を使おうとすることです。ADD と DELETE が存在するのは、
カウンタやセットに対する read-modify-write が、同時実行下で負ける競合だからです。
変更するものによって句を選ぶ
1回の UpdateItem 呼び出しは、SET … REMOVE … ADD … DELETE の順で4つの句すべてを同時に
運べます。各キーワードは最大1回現れ、カンマ区切りのアクションのリストを取ります。
| 句 | 対象 | 用途 |
|---|---|---|
SET | 任意の属性 | 値やドキュメントフィールドを書き込み/上書き |
ADD | 数値かセットのみ | アトミックにインクリメント、またはセットに和集合 |
REMOVE | 任意の属性かリスト要素 | 属性を削除、1つのリストインデックスを落とす |
DELETE | セットのみ | セットから特定のメンバーを取り除く |
文字列に対する ADD やスカラーに対する DELETE は no-op ではなく検証エラーです —
DynamoDB は呼び出し全体を拒否します。
AWS の更新式リファレンス
によれば、ADD は数値とセットに、DELETE はセットに制限されています。
具体例:ショッピングカート
カートごとに1つのアイテムで、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 を増やすには、それを読んでコードで1を足して SET で書き戻し てはいけません。
それは lost-update の競合です。2つの同時実行の追加が両方とも 3 を読み、両方とも 4 を
書き、1つを取りこぼします。ADD は算術をサーバー側で行います。
ADD ItemCount :one
:one = 1 で、これはアトミックなカウンタです。同時実行の呼び出しはアイテム上で直列化
されるので、2つの追加は +2 として着地します。マイナスの数を渡せばデクリメントします。
ItemCount がなければ、ADD はそれをまず 0 として扱います — だからカウンタを初期化
する必要は決してありません。
その正確な式 — 名前、型付き値、そしてマーシャルされたリクエスト — を、1つの #name や
:value プレースホルダーも手でエスケープせずに
DynamoDB Expression Builder で構築できます。
REMOVE — 属性か1つの行アイテムを落とす
REMOVE は属性をまるごと削除する方法です(「null に設定」はありません — それは NULL 型を
書くだけです)。適用済みの割引をクリアし、3つ目の行アイテムを1回の呼び出しで落とします。
REMOVE AppliedDiscount, LineItems[2]
LineItems[2] はインデックス 2 の要素を取り除き、その後のすべてを下にシフト します —
インデックス 3 が 2 になり、以下同様です。1つの式で2つのインデックスを REMOVE すると、
両方が 元の リストに対して評価されるため、[2] と [3] を一緒に取り除くと、期待
どおり3つ目と4つ目の要素が落ちます。
DELETE — セットメンバーを取り除く
PromoCodes は文字列セットなので、顧客が1つのコードを外すときは REMOVE ではなく DELETE
を使います。REMOVE PromoCodes はセット全体を吹き飛ばしますが、DELETE は名指しした
メンバーを差し引きます。
DELETE PromoCodes :pulled
:pulled = セット {"SAVE10"} で、そのメンバーだけが消えます。ここで2つのルールが
噛みます。セットは決して空になれないため、最後のメンバーを削除すると PromoCodes 属性が
まるごと取り除かれます。そして値は属性に一致するセット型でなければなりません — 素の文字列
は型エラーです。
組み合わせる
「アイテムを追加し、プロモを適用し、カウントを増やす」更新は、3つの句にまたがる1回の 呼び出しです。
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を使いましょう — read-modify-write は同時 実行下で更新を取りこぼします。最もよくあるカート/在庫のバグです。 - 存在しないリストに対する
list_append。 ターゲットをif_not_existsで包まないと、 最初の書き込みが失敗します。 REMOVEとDELETEの混同。REMOVEは属性を落とし、DELETEはセットから メンバーを差し引きます。混ぜると意図より多く削除します。UpdateItemがアップサートであることを忘れる。 キーが存在しなければアイテムを作成 します。「更新のみ」を意味するならConditionExpression(attribute_exists(CartPK))を 使いましょう。
これらの式が対象とするキーのモデリングについては シングルテーブル設計 を、カートをどう読み戻すか決めるには Query と Scan を参照してください。
これらのいずれも Expression Builder で構築して コピーし、それから DynoTable を試して 自分のテーブルに対して実行し、アイテムが ライブで変わるのを見てください。