中級読了 2 分

DynamoDB の更新式

更新式は、UpdateItem に単一のアイテムをどう変更するか — どの属性を書き込み、 インクリメントし、削除し、あるいはセットに畳み込むか — を伝えます。UPDATE … SET … WHERE はありません — アイテムをキーで名指しし、4つの句キーワードで変更を記述します。

DynamoDB の更新式はどのように機能しますか?

DynamoDB の更新式は、UpdateItem に4つの句を使って1つのアイテムをどう変更するかを伝えます。SET は属性を書き込むか上書きします。ADD は数値をアトミックにインクリメントするか、セットに和集合します。REMOVE は属性またはリストの1要素を削除します。DELETE はセットから特定のメンバーを取り除きます。1回の呼び出しで4つすべてを同時に指定できます。

  • SET は属性を書き込むか上書きします — スカラー、ドキュメント、そして関数イディオム if_not_existslist_append
  • ADD は、読み取りを先行させずに、1回のラウンドトリップでアトミックな数値の インクリメントかセットの和集合を行います。
  • REMOVE は属性をまるごと削除します(あるいはリストの単一要素をインデックスで)。
  • DELETE はセットから特定のメンバーを取り除きます — セットからのみ。

SQL から来ると、罠はすべてに SET を使おうとすることです。ADDDELETE が存在するのは、 カウンタやセットに対する 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" で キーイングします。実行中の OrderTotalLineItems リスト、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 で包まないと、 最初の書き込みが失敗します。
  • REMOVEDELETE の混同。 REMOVE は属性を落とし、DELETE はセットから メンバーを差し引きます。混ぜると意図より多く削除します。
  • UpdateItem がアップサートであることを忘れる。 キーが存在しなければアイテムを作成 します。「更新のみ」を意味するなら ConditionExpressionattribute_exists(CartPK))を 使いましょう。

これらの式が対象とするキーのモデリングについては シングルテーブル設計 を、カートをどう読み戻すか決めるには Query と Scan を参照してください。

これらのいずれも Expression Builder で構築して コピーし、それから DynoTable を試して 自分のテーブルに対して実行し、アイテムが ライブで変わるのを見てください。

更新日