中級読了 2 分

DynamoDB の Update Expression:SET・REMOVE・ADD・DELETE(例付き)

更新式は UpdateItem に、1 つの項目をどう変更するかを伝えます。どのを書き込み、インクリメントし、削除し、あるいはセットに畳み込むか、です。複数行にわたる UPDATE … SET … WHERE はありません — 完全なキーで単一の項目を名指しし、4 つの句キーワードで変更を記述します。

DynamoDB の更新式はどう動きますか?

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

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

SQL から来ると、罠は何にでも SET に手を伸ばすことです。ADDDELETE が存在するのは、カウンターやセットに対する read-modify-write が、並行実行下では負ける競合だからです。

変更する対象で句を選ぶ

1 回の UpdateItem 呼び出しで、4 つの句すべてを任意の順序で同時に運べます。各キーワードは最大 1 回だけ現れ、カンマ区切りのアクションのリストを取ります。

対象用途
SET任意の属性値やドキュメントフィールドを書き込む/上書きする
ADD数値またはセットのみアトミックにインクリメントする、またはセットに和集合として加える
REMOVE任意の属性またはリスト要素属性を削除する。リストインデックスを 1 つ落とす
DELETEセットのみセットから特定のメンバーを取り除く

文字列への ADD やスカラーへの DELETE は no-op ではなく検証エラーです — DynamoDB は呼び出し全体を拒否します。AWS の更新式リファレンスによれば、ADD は数値とセットに、DELETE はセットに限定されます。

実例: ショッピングカート

カート 1 つにつき項目 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 で書き戻すことは しないでください。それは更新消失の競合です。2 つの同時の追加が両方とも 3 を読み、両方とも 4 を書き、1 つを落とします。ADD は算術をサーバー側で行います。

ADD ItemCount :one

:one = 1 なら、これはアトミックカウンターです。同時呼び出しは項目上で直列化されるので、2 つの追加は +2 として着地します。負の数を渡すとデクリメントします。ItemCount が存在しなければ、ADD はまず 0 として扱う — なのでカウンターをシードする必要はありません。

us-east-1 のオンデマンドでは、ADD は書き込み後のアイテムについて 1 KB あたり 1 WCU(切り上げ)を課金します。ADD ItemCount :one を 1 つ持つ 1 KB のカート要約は 1 WCU です — 同じ行を read-modify-write の SET で書いても WCU は同じですが、並行実行下では負けます。アイテムのサイズはアイテムサイズ計算ツールで測りましょう。

このとおりの式 — 名前、型付き値、そしてマーシャリングされたリクエスト — を、1 つの #name:value プレースホルダーも手でエスケープすることなく、DynamoDB 式ビルダーで構築できます。

下のプリセット — SET とアトミックな ADD — を編集して、UpdateExpression が入力に合わせて再構築されるのを見てください。

リクエストを構築
生成されたコード
new UpdateItemCommand({
  "TableName": "AuditLog",
  "Key": {
    "pk": {
      "S": "TENANT#acme"
    },
    "sk": {
      "S": "CONFIG"
    }
  },
  "UpdateExpression": "SET #upd0 = :updValue0 ADD #upd1 :updValue1",
  "ExpressionAttributeNames": {
    "#upd0": "plan",
    "#upd1": "seats"
  },
  "ExpressionAttributeValues": {
    ":updValue0": {
      "S": "pro"
    },
    ":updValue1": {
      "N": "5"
    }
  }
})

REMOVE — 属性やライン項目を 1 つ落とす

REMOVE は属性をまるごと削除する方法です(「null にセット」はありません — それは単に NULL 型を書き込むだけです)。適用済みの割引をクリアし、3 番目のライン項目を 1 回の呼び出しで落とします。

REMOVE AppliedDiscount, LineItems[2]

LineItems[2] はインデックス 2 の要素を削除し、その後のすべてを 1 つ下へシフト します — インデックス 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 と同じくらいアトミックで競合安全です。DynamoDB は現在の値に対してサーバー側で OrderTotal + :price を評価するので、同時呼び出しはあなたのコードを往復するのではなく項目上で直列化されます。

避けるべき落とし穴

  • 先に読み取ったカウンターを SET する。 ADD を使ってください — read-modify-write は並行実行下で更新を失います。これは最もよくあるカート/在庫のバグです。
  • 存在しないリストへの list_append 対象を if_not_exists で包まないと、最初の書き込みが失敗します。
  • REMOVEDELETE の混同。 REMOVE は属性を落とします。DELETE はセットからメンバーを差し引きます。これらを取り違えると、意図した以上に削除します。
  • UpdateItem がアップサートであることを忘れる。 キーが存在しなければ、項目を作成します。「更新だけ」を意図するなら ConditionExpressionattribute_exists(CartPK))を使ってください。

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

これらのいずれも式ビルダーで構築してコピーし、DynoTable を試して自分自身のテーブルに対して実行し、項目がライブで変わるのを見てください。

更新日