- AWS
DynamoDB UpdateExpressionの書き方|SET・ADD・条件
UpdateExpression は、DynamoDB の update_item(CLI では update-item)で「どの属性をどう変えるか」を書く式です。 SET(値を入れる)、REMOVE(属性を消す)、ADD(数値に足す・セットに加える)、DELETE(セットから取り除く)の4つの句を組み合わせて書きます。
この記事では、4つの句の違いを早見表で整理し、それぞれの更新前後のアイテムを boto3 と AWS CLI の両方で示します。関数(if_not_exists・list_append)、ReturnValues の5種類、条件式と楽観ロック、よく出るエラーの文面と直し方まで、すべて実行結果つきで載せています。
確認環境:Ubuntu 24.04、Python 3.11.15、boto3 1.43.103、AWS CLI 1.46.1。AWS の API をローカルで再現するツール(moto 5.2.3)で実行しています。掲載するコードとコマンドは、実際の AWS でそのまま使える形(endpoint の指定なし)で書いています。
SET・REMOVE・ADD・DELETEの早見表
| 句 | すること | 対象の型 | 属性がないとき | 典型的な用途 |
|---|---|---|---|---|
| SET | 値を入れる(上書き・新規追加) | すべての型 | 新しく作られる。ただし price = price + :n のように自分自身を参照する式はエラー | 値の更新、加算・減算、リストへの追加、ネストの更新 |
| REMOVE | 属性やリストの要素を消す | すべての型 | 何も起きない(エラーにならない) | 不要になった属性の削除 |
| ADD | 数値に足す/セットに要素を加える | 数値、セット(SS・NS・BS) | 数値:渡した値で作られる。セット:渡したセットで作られる | カウンタ、タグの追加 |
| DELETE | セットから要素を取り除く | セットのみ | 何も起きない | タグの削除 |
書き方の形は次のとおりです。
| 句 | 書き方 | 例 |
|---|---|---|
| SET | SET パス = 値, パス = 値 | SET price = :p, spec.color = :c |
| REMOVE | REMOVE パス, パス | REMOVE note, history[0] |
| ADD | ADD パス 値, パス 値(= は書かない) | ADD stock :n, tags :t |
| DELETE | DELETE パス 値(= は書かない) | DELETE tags :t |
SET で使える関数と演算です。
| 書き方 | 意味 |
|---|---|
| SET a = :v | 値を入れる |
| SET a = a + :n / a - :n | 今の値に足す/引く |
| SET a = if_not_exists(a, :v) | a がないときだけ :v を入れる |
| SET a = if_not_exists(a, :zero) + :n | ないときは 0 から数える加算カウンタ |
| SET a = list_append(a, :list) | リストの末尾に追加 |
| SET a = list_append(:list, a) | リストの先頭に追加 |
| SET a.b = :v / a[0] = :v | マップの中の属性/リストの要素を更新 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「DynamoDB UpdateExpressionの書き方|SET・ADD・条件」
準備:テーブルと共通のコード
以降の例は、パーティションキーが id(文字列)の Items テーブルと、次の共通コード(common.py)を使います。毎回同じ初期データに戻してから更新するので、結果をそのまま見比べられます。
import json
from decimal import Decimal
import boto3
dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1")
TABLE = "Items"
def create_table():
client = dynamodb.meta.client
if TABLE in client.list_tables()["TableNames"]:
dynamodb.Table(TABLE).delete()
client.get_waiter("table_not_exists").wait(TableName=TABLE)
dynamodb.create_table(
TableName=TABLE,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
)
client.get_waiter("table_exists").wait(TableName=TABLE)
return dynamodb.Table(TABLE)
def reset(table):
"""毎回同じ初期データに戻す"""
table.put_item(Item={
"id": "item-1",
"name": "desk",
"price": 12000,
"stock": 5,
"tags": {"wood", "office"}, # 文字列セット(SS)
"history": ["created"], # リスト(L)
"spec": {"color": "brown", "size": {"w": 120, "d": 60}}, # マップ(M)
"note": "temp",
"version": 1,
})
def _plain(o):
if isinstance(o, Decimal):
return int(o) if o == o.to_integral_value() else float(o)
if isinstance(o, set):
return {"(set)": sorted(int(x) if isinstance(x, Decimal) else x for x in o)}
raise TypeError(type(o))
def show(label, obj):
print(label, json.dumps(obj, default=_plain, ensure_ascii=False, sort_keys=True))
def get(table, key="item-1"):
return table.get_item(Key={"id": key}).get("Item")
boto3 の resource(Table)を使うと、Python の値(文字列・数値・set・list・dict)をそのまま渡せます。表示では、セットを {"(set)": [...]} と表しています。
:name のようにコロンで始まるものは値のプレースホルダーで、ExpressionAttributeValues で中身を渡します。#n のようにシャープで始まるものは属性名のプレースホルダーで、ExpressionAttributeNames で実際の名前を渡します。
SET:値を入れる・足す・リストに追加する
from common import create_table, reset, show, get
table = create_table()
reset(table)
show("更新前:", get(table))
# 1. 値を上書きする/新しい属性を足す(name は予約語なので #n に置き換える)
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET #n = :name, category = :cat",
ExpressionAttributeNames={"#n": "name"},
ExpressionAttributeValues={":name": "standing desk", ":cat": "furniture"},
)
show("1 上書きと追加:", get(table))
# 2. 今の値に足す・引く(加算カウンタ)
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET price = price + :up, stock = stock - :sold",
ExpressionAttributeValues={":up": 500, ":sold": 2},
)
show("2 加算と減算:", {k: get(table)[k] for k in ("price", "stock")})
# 3. if_not_exists:属性がないときだけ初期値を入れる
for i in (1, 2):
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET created_by = if_not_exists(created_by, :u), view_count = if_not_exists(view_count, :zero) + :one",
ExpressionAttributeValues={":u": f"user-{i}", ":zero": 0, ":one": 1},
)
show(f"3 if_not_exists {i}回目:", {k: get(table)[k] for k in ("created_by", "view_count")})
# 4. list_append:リストの末尾・先頭に足す
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET history = list_append(history, :tail)",
ExpressionAttributeValues={":tail": ["priced", "stocked"]},
)
show("4 末尾に追加:", get(table)["history"])
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET history = list_append(:head, history)",
ExpressionAttributeValues={":head": ["draft"]},
)
show("4 先頭に追加:", get(table)["history"])
# 5. リストがまだないかもしれないとき:if_not_exists と組み合わせる
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET comments = list_append(if_not_exists(comments, :empty), :c)",
ExpressionAttributeValues={":empty": [], ":c": ["first comment"]},
)
show("5 ないリストに追加:", get(table)["comments"])
# 6. ネストした属性・リストの要素を更新する
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET spec.color = :c, spec.#s.h = :h, history[0] = :first",
ExpressionAttributeNames={"#s": "size"},
ExpressionAttributeValues={":c": "white", ":h": 72, ":first": "planned"},
)
item = get(table)
show("6 ネストの更新:", {"spec": item["spec"], "history": item["history"]})
更新前: {"history": ["created"], "id": "item-1", "name": "desk", "note": "temp", "price": 12000, "spec": {"color": "brown", "size": {"d": 60, "w": 120}}, "stock": 5, "tags": {"(set)": ["office", "wood"]}, "version": 1}
1 上書きと追加: {"category": "furniture", "history": ["created"], "id": "item-1", "name": "standing desk", "note": "temp", "price": 12000, "spec": {"color": "brown", "size": {"d": 60, "w": 120}}, "stock": 5, "tags": {"(set)": ["office", "wood"]}, "version": 1}
2 加算と減算: {"price": 12500, "stock": 3}
3 if_not_exists 1回目: {"created_by": "user-1", "view_count": 1}
3 if_not_exists 2回目: {"created_by": "user-1", "view_count": 2}
4 末尾に追加: ["created", "priced", "stocked"]
4 先頭に追加: ["draft", "created", "priced", "stocked"]
5 ないリストに追加: ["first comment"]
6 ネストの更新: {"history": ["planned", "created", "priced", "stocked"], "spec": {"color": "white", "size": {"d": 60, "h": 72, "w": 120}}}
ポイントは次のとおりです。
- SET は属性があれば上書き、なければ追加します(1)
- price = price + :up のように、今の値を使った計算ができます(2)。読み出してから書き戻す必要がなく、1回のリクエストで済みます
- if_not_exists(属性, 初期値) は、属性があればその値、なければ初期値になります。created_by は2回目も user-1 のまま、view_count は「なければ 0」から数え始めて 1 → 2 と増えています(3)
- list_append は引数の順番で、末尾に足すか先頭に足すかが決まります(4)。追加する側は、要素が1つでもリストで渡します
- spec.color のようにドットでマップの中、history[0] のように添字でリストの要素を指定できます(6)。size は予約語なので #s に置き換えています
REMOVE・ADD・DELETE と、4つをまとめた更新
from common import create_table, reset, show, get
table = create_table()
reset(table)
show("更新前:", get(table))
# --- REMOVE:属性やリストの要素を消す ---
table.update_item(
Key={"id": "item-1"},
UpdateExpression="REMOVE note, spec.#s.d, no_such_attr", # ない属性を指定してもエラーにならない
ExpressionAttributeNames={"#s": "size"},
)
show("REMOVE 後:", get(table))
table.update_item(Key={"id": "item-1"}, UpdateExpression="SET history = :h",
ExpressionAttributeValues={":h": ["a", "b", "c", "d"]})
table.update_item(Key={"id": "item-1"}, UpdateExpression="REMOVE history[1], history[2]")
show("REMOVE history[1], history[2] 後:", get(table)["history"])
# --- ADD:数値に足す/セットに要素を足す ---
table.update_item(
Key={"id": "item-1"},
UpdateExpression="ADD stock :n, sold_count :n, tags :t, colors :c",
ExpressionAttributeValues={":n": 3, ":t": {"sale", "wood"}, ":c": {"white"}},
)
item = get(table)
show("ADD 後:", {k: item[k] for k in ("stock", "sold_count", "tags", "colors")})
table.update_item(Key={"id": "item-1"}, UpdateExpression="ADD stock :n",
ExpressionAttributeValues={":n": -10})
show("ADD に負の数:", {"stock": get(table)["stock"]})
# --- DELETE:セットから要素を取り除く ---
table.update_item(
Key={"id": "item-1"},
UpdateExpression="DELETE tags :t",
ExpressionAttributeValues={":t": {"sale", "no-such-tag"}}, # ない要素は無視される
)
show("DELETE 後:", {"tags": get(table)["tags"]})
table.update_item(Key={"id": "item-1"}, UpdateExpression="DELETE colors :c",
ExpressionAttributeValues={":c": {"white"}})
show("最後の1要素を DELETE:", {"colors があるか": "colors" in get(table)})
# --- 4つを1回の更新にまとめる ---
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET price = :p, updated_by = :u REMOVE history ADD version :one DELETE tags :t",
ExpressionAttributeValues={":p": 9800, ":u": "admin", ":one": 1, ":t": {"office"}},
)
show("まとめて更新:", get(table))
更新前: {"history": ["created"], "id": "item-1", "name": "desk", "note": "temp", "price": 12000, "spec": {"color": "brown", "size": {"d": 60, "w": 120}}, "stock": 5, "tags": {"(set)": ["office", "wood"]}, "version": 1}
REMOVE 後: {"history": ["created"], "id": "item-1", "name": "desk", "price": 12000, "spec": {"color": "brown", "size": {"w": 120}}, "stock": 5, "tags": {"(set)": ["office", "wood"]}, "version": 1}
REMOVE history[1], history[2] 後: ["a", "d"]
ADD 後: {"colors": {"(set)": ["white"]}, "sold_count": 3, "stock": 8, "tags": {"(set)": ["office", "sale", "wood"]}}
ADD に負の数: {"stock": -2}
DELETE 後: {"tags": {"(set)": ["office", "wood"]}}
最後の1要素を DELETE: {"colors があるか": false}
まとめて更新: {"id": "item-1", "name": "desk", "price": 9800, "sold_count": 3, "spec": {"color": "brown", "size": {"w": 120}}, "stock": -2, "tags": {"(set)": ["wood"]}, "updated_by": "admin", "version": 2}
- REMOVE は、ない属性を指定してもエラーになりません。リストの要素を消すと、残りの要素が前に詰まります。history[1] と history[2] は「更新前の位置」で解釈されるので、b と c が消えて a と d が残ります
- ADD は、属性がなくても動きます。sold_count はなかったので 3 で作られ、colors はなかったので渡したセットで作られました。セットへの ADD は、すでにある要素(wood)を重複させません
- ADD に負の数を渡すと引き算になります
- DELETE は、セットにない要素を指定しても無視されます。最後の1要素を取り除くと、属性そのものがなくなります
- 4つの句は1回の更新にまとめて書けます。句の間はスペースで区切り、同じ句の中はカンマで区切ります
ADD と SET の加算の使い分け
| やりたいこと | 書き方 | 属性がないとき |
|---|---|---|
| 数値に足す(属性は必ずある) | SET stock = stock + :n | エラー |
| 数値に足す(ないときは 0 から) | SET stock = if_not_exists(stock, :zero) + :n | :n になる |
| 数値に足す(短く書く) | ADD stock :n | :n になる |
| セットに要素を足す | ADD tags :t | 渡したセットで作られる |
| リストに要素を足す | SET history = list_append(history, :l) | エラー(if_not_exists と組み合わせる) |
DynamoDB の開発者ガイドには、ADD が対応するのは数値型とセット型だけであること、DELETE が対応するのはセット型だけであること、そして「一般に、ADD よりも SET を使うことをすすめる」ことが書かれています。数値の加算は SET と if_not_exists の組み合わせで書けるので、セットへの追加に ADD を使う、と分けると整理しやすくなります。
AWS CLIで同じ4つを書く
CLI では、値を {"S": "文字列"}、{"N": "数値"}、{"SS": [...]} のように型つきで渡します。
# 更新前のアイテムを入れる
aws dynamodb put-item --table-name Items --item '{
"id": {"S": "item-2"},
"name": {"S": "chair"},
"price": {"N": "8000"},
"stock": {"N": "10"},
"tags": {"SS": ["office", "wood"]},
"note": {"S": "temp"}
}'
# SET:名前を変え、価格に500を足す
aws dynamodb update-item --table-name Items --key '{"id":{"S":"item-2"}}' \
--update-expression 'SET #n = :name, price = price + :up' \
--expression-attribute-names '{"#n":"name"}' \
--expression-attribute-values '{":name":{"S":"office chair"},":up":{"N":"500"}}'
# REMOVE:note を消す
aws dynamodb update-item --table-name Items --key '{"id":{"S":"item-2"}}' \
--update-expression 'REMOVE note'
# ADD:在庫に5を足し、タグに sale を加える
aws dynamodb update-item --table-name Items --key '{"id":{"S":"item-2"}}' \
--update-expression 'ADD stock :n, tags :t' \
--expression-attribute-values '{":n":{"N":"5"},":t":{"SS":["sale"]}}'
# DELETE:タグから wood を取り除き、更新後の値を受け取る
aws dynamodb update-item --table-name Items --key '{"id":{"S":"item-2"}}' \
--update-expression 'DELETE tags :t' \
--expression-attribute-values '{":t":{"SS":["wood"]}}' \
--return-values UPDATED_NEW --query Attributes
各コマンドのあとに get-item で取り出したアイテムです(1行にまとめて表示)。
# 更新前
{"id": {"S": "item-2"}, "name": {"S": "chair"}, "note": {"S": "temp"}, "price": {"N": "8000"}, "stock": {"N": "10"}, "tags": {"SS": ["office", "wood"]}}
# SET のあと
{"id": {"S": "item-2"}, "name": {"S": "office chair"}, "note": {"S": "temp"}, "price": {"N": "8500"}, "stock": {"N": "10"}, "tags": {"SS": ["office", "wood"]}}
# REMOVE のあと
{"id": {"S": "item-2"}, "name": {"S": "office chair"}, "price": {"N": "8500"}, "stock": {"N": "10"}, "tags": {"SS": ["office", "wood"]}}
# ADD のあと
{"id": {"S": "item-2"}, "name": {"S": "office chair"}, "price": {"N": "8500"}, "stock": {"N": "15"}, "tags": {"SS": ["office", "wood", "sale"]}}
# DELETE のあと
{"id": {"S": "item-2"}, "name": {"S": "office chair"}, "price": {"N": "8500"}, "stock": {"N": "15"}, "tags": {"SS": ["office", "sale"]}}
最後の DELETE のコマンド自体の出力(--return-values UPDATED_NEW)は次のとおりです。
{
"tags": {
"SS": [
"office",
"sale"
]
}
}
| 項目 | boto3(resource) | AWS CLI |
|---|---|---|
| 式 | UpdateExpression="..." | --update-expression '...' |
| 属性名の置き換え | ExpressionAttributeNames={"#n": "name"} | --expression-attribute-names '{"#n":"name"}' |
| 値 | ExpressionAttributeValues={":up": 500} | --expression-attribute-values '{":up":{"N":"500"}}' |
| 条件 | ConditionExpression="..." | --condition-expression '...' |
| 戻り値 | ReturnValues="UPDATED_NEW" | --return-values UPDATED_NEW |
boto3 の client(boto3.client("dynamodb"))を使う場合は、値の書き方が CLI と同じ型つきの形になります。
ReturnValues:5種類の戻り値の違い
update_item は、既定では更新結果を返しません。ReturnValues を指定すると、更新前または更新後の属性を、同じリクエストの応答で受け取れます。 同じ更新(price に加算、category を追加、note を削除)を5種類で実行した結果です。
from common import create_table, reset, show
table = create_table()
for rv in ("NONE", "ALL_OLD", "UPDATED_OLD", "ALL_NEW", "UPDATED_NEW"):
reset(table)
res = table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET price = price + :up, category = :cat REMOVE note",
ExpressionAttributeValues={":up": 500, ":cat": "furniture"},
ReturnValues=rv,
)
show(f"{rv:<12}:", res.get("Attributes", "(Attributes キーなし)"))
NONE : "(Attributes キーなし)"
ALL_OLD : {"history": ["created"], "id": "item-1", "name": "desk", "note": "temp", "price": 12000, "spec": {"color": "brown", "size": {"d": 60, "w": 120}}, "stock": 5, "tags": {"(set)": ["office", "wood"]}, "version": 1}
UPDATED_OLD : {"note": "temp", "price": 12000}
ALL_NEW : {"category": "furniture", "history": ["created"], "id": "item-1", "name": "desk", "price": 12500, "spec": {"color": "brown", "size": {"d": 60, "w": 120}}, "stock": 5, "tags": {"(set)": ["office", "wood"]}, "version": 1}
UPDATED_NEW : {"category": "furniture", "price": 12500}
| ReturnValues | 返るもの | 使いどころ |
|---|---|---|
| NONE(既定) | 何も返らない(Attributes キーがない) | 結果が要らないとき |
| ALL_OLD | 更新前のアイテム全体 | 変更前の状態を記録に残す |
| UPDATED_OLD | 更新した属性だけの、更新前の値 | 何から変わったかを知る |
| ALL_NEW | 更新後のアイテム全体 | 更新後の最新のアイテムをそのまま使う |
| UPDATED_NEW | 更新した属性だけの、更新後の値 | カウンタの新しい値を受け取る |
REMOVE した note は、UPDATED_OLD には入り、UPDATED_NEW には入りません(更新後には存在しないため)。逆に、新しく追加した category は UPDATED_NEW にだけ入ります。
API リファレンスによると、戻り値を要求しても読み取りキャパシティユニットは消費されず、返る値は強い整合性のあるものです。加算したあとの値が必要なときは、get_item で読み直すのではなく UPDATED_NEW で受け取ると、1回のリクエストで済みます。
条件つき更新(ConditionExpression)
条件なしの update_item は「なければ作る」
API リファレンスに「UpdateItem は既存アイテムの属性を編集し、アイテムがなければ新しく追加する」とあるとおり、存在しないキーを指定すると新しいアイテムが作られます。既存のアイテムだけを更新したいときは、ConditionExpression に attribute_exists(キー属性) を付けます。
from botocore.exceptions import ClientError
from common import create_table, reset, show, get, dynamodb
table = create_table()
reset(table)
# 1. 条件なし:存在しないキーを更新すると、新しいアイテムが作られる
table.update_item(Key={"id": "item-999"}, UpdateExpression="SET stock = :s",
ExpressionAttributeValues={":s": 1})
show("1 条件なし:", get(table, "item-999"))
table.delete_item(Key={"id": "item-999"})
# 2. attribute_exists:あるアイテムだけ更新する
try:
table.update_item(
Key={"id": "item-999"},
UpdateExpression="SET stock = :s",
ConditionExpression="attribute_exists(id)",
ExpressionAttributeValues={":s": 1},
)
except ClientError as e:
print("2 attribute_exists:", e.response["Error"]["Code"], "/", e.response["Error"]["Message"])
show("2 作られていない:", get(table, "item-999"))
# 3. 在庫が足りるときだけ減らす
def sell(qty):
try:
res = table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET stock = stock - :q",
ConditionExpression="stock >= :q",
ExpressionAttributeValues={":q": qty},
ReturnValues="UPDATED_NEW",
)
return f"{qty}個 → 残り {res['Attributes']['stock']}"
except dynamodb.meta.client.exceptions.ConditionalCheckFailedException:
return f"{qty}個 → 在庫が足りないため更新しない"
print("3", sell(3))
print("3", sell(3))
# 4. 条件に合わなかったときの今の値を受け取る
try:
table.update_item(
Key={"id": "item-1"},
UpdateExpression="SET stock = stock - :q",
ConditionExpression="stock >= :q",
ExpressionAttributeValues={":q": 3},
ReturnValuesOnConditionCheckFailure="ALL_OLD",
)
except ClientError as e:
print("4 Code:", e.response["Error"]["Code"])
print("4 Item:", e.response.get("Item"))
# 5. 条件式で使える主な書き方
checks = {
"attribute_not_exists(deleted_at)": {},
"begins_with(#n, :p)": {"names": {"#n": "name"}, "values": {":p": "de"}},
"contains(tags, :t)": {"values": {":t": "wood"}},
"price BETWEEN :lo AND :hi": {"values": {":lo": 10000, ":hi": 20000}},
"size(history) < :max": {"values": {":max": 5}},
"attribute_type(spec, :m)": {"values": {":m": "M"}},
"stock > :z AND NOT contains(tags, :t)": {"values": {":z": 0, ":t": "wood"}},
}
for cond, opt in checks.items():
kwargs = dict(Key={"id": "item-1"}, UpdateExpression="SET checked = :c", ConditionExpression=cond,
ExpressionAttributeValues={":c": True, **opt.get("values", {})})
if "names" in opt:
kwargs["ExpressionAttributeNames"] = opt["names"]
try:
table.update_item(**kwargs)
print(f"5 {cond:<40} → 更新された")
except ClientError as e:
print(f"5 {cond:<40} → {e.response['Error']['Code']}")
1 条件なし: {"id": "item-999", "stock": 1}
2 attribute_exists: ConditionalCheckFailedException / The conditional request failed
2 作られていない: null
3 3個 → 残り 2
3 3個 → 在庫が足りないため更新しない
4 Code: ConditionalCheckFailedException
4 Item: {'id': {'S': 'item-1'}, 'name': {'S': 'desk'}, 'price': {'N': '12000'}, 'stock': {'N': '2'}, 'tags': {'SS': ['wood', 'office']}, 'history': {'L': [{'S': 'created'}]}, 'spec': {'M': {'color': {'S': 'brown'}, 'size': {'M': {'w': {'N': '120'}, 'd': {'N': '60'}}}}}, 'note': {'S': 'temp'}, 'version': {'N': '1'}}
5 attribute_not_exists(deleted_at) → 更新された
5 begins_with(#n, :p) → 更新された
5 contains(tags, :t) → 更新された
5 price BETWEEN :lo AND :hi → 更新された
5 size(history) < :max → 更新された
5 attribute_type(spec, :m) → 更新された
5 stock > :z AND NOT contains(tags, :t) → ConditionalCheckFailedException
- 条件に合わないと ConditionalCheckFailedException になり、アイテムは変更されません(2)
- 「在庫が足りるときだけ減らす」は stock >= :q を条件にします(3)。確認と更新が1回のリクエストで行われるので、同時に複数の更新が来ても在庫がマイナスになりません
- ReturnValuesOnConditionCheckFailure="ALL_OLD" を付けると、条件に合わなかったときの今のアイテムが例外の response["Item"] に入ります(4)。型つきの形({"N": "2"} など)で返る点に注意してください
- 例外は、ClientError の Code で見分ける方法と、client.exceptions.ConditionalCheckFailedException で受ける方法のどちらでも書けます
条件式で使える書き方
| 書き方 | 意味 |
|---|---|
| attribute_exists(a) | 属性 a がある |
| attribute_not_exists(a) | 属性 a がない |
a = :v、a <> :v、a < :v、a >= :v |
比較 |
| a BETWEEN :lo AND :hi | 範囲 |
| a IN (:x, :y) | いずれかと等しい |
| begins_with(a, :prefix) | 文字列が :prefix で始まる |
| contains(a, :v) | 文字列に含む/セット・リストに要素がある |
size(a) < :n |
文字列の長さ、リスト・セットの要素数 |
| attribute_type(a, :t) | 型が :t(S、N、M、L、SS など) |
| 条件 AND 条件、条件 OR 条件、NOT 条件 | 組み合わせ |
CLI での条件つき更新
aws dynamodb update-item --table-name Items --key '{"id":{"S":"item-2"}}' \
--update-expression 'SET stock = stock - :q' \
--condition-expression 'stock >= :q' \
--expression-attribute-values '{":q":{"N":"100"}}'
An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: The conditional request failed
終了コードは255です。シェルスクリプトでは、終了コードで「条件に合わなかった」ことを判定できます。
楽観ロック:version 属性で更新の衝突を防ぐ
2人が同じアイテムを読み、それぞれ書き戻すと、後から書いたほうが先の更新を上書きしてしまいます。アイテムに version 属性を持たせ、「読んだときの version と同じなら更新し、同時に version を1つ進める」と書くと、先に誰かが更新していた場合に気づけます。 これが楽観ロックです。
from botocore.exceptions import ClientError
from common import create_table, reset, show, get
table = create_table()
reset(table)
def update_price(item, new_price, who):
"""読んだときの version と同じなら更新し、version を1つ進める"""
try:
table.update_item(
Key={"id": item["id"]},
UpdateExpression="SET price = :p, updated_by = :w, version = version + :one",
ConditionExpression="version = :expected",
ExpressionAttributeValues={
":p": new_price, ":w": who, ":one": 1,
":expected": item["version"],
},
)
return True
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return False
raise
# AさんとBさんが、同じ時点のアイテム(version=1)を読む
a = get(table)
b = get(table)
print("A の更新:", update_price(a, 13000, "A"))
print("B の更新:", update_price(b, 11000, "B"), "(A が先に更新したので version が合わない)")
show("この時点:", {k: get(table)[k] for k in ("price", "updated_by", "version")})
# B は読み直してからやり直す
for attempt in range(1, 4):
b = get(table)
if update_price(b, 11000, "B"):
print(f"B の再試行 {attempt} 回目: True")
break
show("最終結果:", {k: get(table)[k] for k in ("price", "updated_by", "version")})
A の更新: True
B の更新: False (A が先に更新したので version が合わない)
この時点: {"price": 13000, "updated_by": "A", "version": 2}
B の再試行 1 回目: True
最終結果: {"price": 11000, "updated_by": "B", "version": 3}
sequenceDiagram
participant A
participant B
participant D as DynamoDB
A->>D: 読む(version=1)
B->>D: 読む(version=1)
A->>D: 更新(条件 version = 1)→ 成功、version=2
B->>D: 更新(条件 version = 1)→ ConditionalCheckFailedException
B->>D: 読み直す(version=2)
B->>D: 更新(条件 version = 2)→ 成功、version=3
条件に合わなかったときは、読み直してから「やり直す」か「利用者に最新の内容を見せて判断してもらう」かを、扱うデータに合わせて決めます。
よく出るエラーと直し方
式の内容に関するエラー(ValidationException)
from botocore.exceptions import ClientError
from common import create_table, reset, show, get
table = create_table()
reset(table)
def attempt(label, **kwargs):
try:
table.update_item(Key={"id": "item-1"}, **kwargs)
print(f"[{label}] 成功")
except ClientError as e:
err = e.response["Error"]
print(f"[{label}] {err['Code']}: {err['Message']}")
# 1. 予約語をそのまま書く
attempt("予約語", UpdateExpression="SET name = :v", ExpressionAttributeValues={":v": "x"})
# 2. ない属性に加算する
attempt("ない属性に加算", UpdateExpression="SET points = points + :n", ExpressionAttributeValues={":n": 1})
# 3. 親のマップがないネストを更新する
attempt("親がないネスト", UpdateExpression="SET options.gift = :v", ExpressionAttributeValues={":v": True})
# 4. 文字列の属性に ADD する
attempt("文字列にADD", UpdateExpression="ADD note :n", ExpressionAttributeValues={":n": 1})
# 5. リストに DELETE する
attempt("リストにDELETE", UpdateExpression="DELETE history :s", ExpressionAttributeValues={":s": {"created"}})
# 6. 値のプレースホルダーを渡し忘れる
attempt("値の渡し忘れ", UpdateExpression="SET price = :p")
# 7. キー属性を更新する
attempt("キーの更新", UpdateExpression="SET id = :v", ExpressionAttributeValues={":v": "item-2"})
# 8. リストの範囲外の要素に SET する(エラーにならず末尾に入る)
attempt("範囲外の要素", UpdateExpression="SET history[10] = :v", ExpressionAttributeValues={":v": "far"})
show(" history:", get(table)["history"])
[予約語] ValidationException: Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: name
[ない属性に加算] ValidationException: The provided expression refers to an attribute that does not exist in the item
[親がないネスト] ValidationException: The document path provided in the update expression is invalid for update
[文字列にADD] ValidationException: An operand in the update expression has an incorrect data type
[リストにDELETE] ValidationException: An operand in the update expression has an incorrect data type
[値の渡し忘れ] ValidationException: Invalid UpdateExpression: An expression attribute value used in expression is not defined; attribute value: :p
[キーの更新] ValidationException: One or more parameter values were invalid: Cannot update attribute id. This attribute is part of the key
[範囲外の要素] 成功
history: ["created", "far"]
| 場面 | メッセージの要点 | 直し方 |
|---|---|---|
| 予約語 | Attribute name is a reserved keyword | 属性名を #n などに置き換え、ExpressionAttributeNames で渡す |
| ない属性に加算 | refers to an attribute that does not exist in the item | if_not_exists(属性, :zero) + :n にする |
| 親がないネスト | The document path ... is invalid for update | 先に親のマップを作る(SET options = :m)か、if_not_exists で親ごと入れる |
| 文字列に ADD/リストに DELETE | An operand ... has an incorrect data type | ADD は数値とセット、DELETE はセットにだけ使う。リストは list_append と REMOVE |
| 値の渡し忘れ | An expression attribute value used in expression is not defined | ExpressionAttributeValues に :p を足す |
| キーの更新 | Cannot update attribute id. This attribute is part of the key | キーは変更できない。新しいキーで put し、元のアイテムを delete する |
予約語は name のほかにも多く、status、size、count、views なども含まれます。 予約語かどうか迷う属性名は、最初から # で置き換えておくと確実です。なお、リストの範囲外の添字に SET した場合はエラーにならず、末尾に追加されます(8)。開発者ガイドにも、要素がまだなければ SET はリストの末尾に追加すると書かれています。
パスの重複(Two document paths overlap)と、句の書きすぎ
1回の更新の中で、同じ属性(または親と子)を2回書くことはできません。
from botocore.exceptions import ClientError
from common import create_table, reset
table = create_table()
reset(table)
def attempt(label, **kwargs):
try:
table.update_item(Key={"id": "item-1"}, **kwargs)
print(f"[{label}] 成功")
except ClientError as e:
err = e.response["Error"]
# メッセージのうち「Invalid UpdateExpression: 」より後ろを表示する
detail = err["Message"].split("Invalid UpdateExpression: ")[-1]
print(f"[{label}] {err['Code']}\n {detail}")
# 1. 親と子を同時に更新する
attempt("親と子", UpdateExpression="SET spec = :m, spec.color = :c",
ExpressionAttributeValues={":m": {"color": "red"}, ":c": "blue"})
# 2. 同じ属性を SET に2回書く
attempt("同じ属性を2回", UpdateExpression="SET price = :a, price = :b",
ExpressionAttributeValues={":a": 1, ":b": 2})
# 3. 同じ属性を SET と REMOVE の両方に書く
attempt("SETとREMOVE", UpdateExpression="SET note = :a REMOVE note",
ExpressionAttributeValues={":a": "x"})
# 4. SET を2回書く
attempt("SETを2回", UpdateExpression="SET price = :a SET stock = :b",
ExpressionAttributeValues={":a": 1, ":b": 2})
# 直した形:親は1回だけ、SET は1つにまとめてカンマで区切る
attempt("直した形", UpdateExpression="SET spec.color = :c, price = :a, stock = :b",
ExpressionAttributeValues={":c": "blue", ":a": 1, ":b": 2})
[親と子] ValidationException
Two document paths overlap with each other; must remove or rewrite one of these paths; path one: [spec], path two: [spec, color]
[同じ属性を2回] ValidationException
Two document paths overlap with each other; must remove or rewrite one of these paths; path one: [price], path two: [price]
[SETとREMOVE] ValidationException
Two document paths overlap with each other; must remove or rewrite one of these paths; path one: [note], path two: [note]
[SETを2回] ValidationException
The "SET" section can only be used once in an update expression;
[直した形] 成功
| エラーになる書き方 | 理由 | 直した書き方 |
|---|---|---|
| SET spec = :m, spec.color = :c | 親(spec)と子(spec.color)を同時に更新している | SET spec = :m だけにして、:m の中に color を入れる |
| SET price = :a, price = :b | 同じ属性を2回書いている | どちらか1つにする |
| SET note = :a REMOVE note | 別の句でも、同じ属性は1回まで | どちらか1つにする |
| SET price = :a SET stock = :b | 同じ句(SET)は1回まで | SET price = :a, stock = :b とカンマでつなぐ |
開発者ガイドにも、各句はどの順番で書いてもよいが、それぞれのキーワードは1回だけ使える、とあります。
予約語のエラーを CLI で見る
aws dynamodb update-item --table-name Items --key '{"id":{"S":"item-2"}}' \
--update-expression 'SET name = :name' \
--expression-attribute-values '{":name":{"S":"x"}}'
An error occurred (ValidationException) when calling the UpdateItem operation: Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: name
やりたいこと別の逆引き表
| やりたいこと | UpdateExpression | あわせて指定するもの |
|---|---|---|
| 値を更新する | SET price = :p | |
| 複数の属性を更新する | SET price = :p, stock = :s | |
| 予約語の属性を更新する | SET #n = :v | ExpressionAttributeNames={"#n": "name"} |
| カウンタを1増やす(ないときは1から) | SET cnt = if_not_exists(cnt, :zero) + :one | ReturnValues="UPDATED_NEW" |
| 最初の1回だけ値を入れる | SET created_at = if_not_exists(created_at, :now) | |
| リストの末尾に追加する | SET logs = list_append(if_not_exists(logs, :empty), :l) | |
| リストの先頭の要素を消す | REMOVE logs[0] | |
| マップの中の1つを更新する | SET spec.color = :c | |
| 属性を消す | REMOVE note | |
| タグを足す | ADD tags :t | :t はセット |
| タグを外す | DELETE tags :t | :t はセット |
| あるアイテムだけ更新する | (任意の式) | ConditionExpression="attribute_exists(id)" |
| 在庫が足りるときだけ減らす | SET stock = stock - :q | ConditionExpression="stock >= :q" |
| 楽観ロック | SET ..., version = version + :one | ConditionExpression="version = :expected" |
よくある質問
Q. SET と ADD のどちらで加算すればよいですか? A. どちらでも加算できます。開発者ガイドは、一般に ADD より SET をすすめています。属性がないときも動かしたい場合は SET cnt = if_not_exists(cnt, :zero) + :n と書きます。セットに要素を足すときは ADD を使います。
Q. REMOVE と DELETE の違いは何ですか? A. REMOVE は属性そのもの(またはリストの要素)を消します。DELETE はセット型の属性から、指定した要素だけを取り除きます。
Q. update_item で存在しないキーを指定するとどうなりますか? A. 新しいアイテムが作られます。既存のアイテムだけを更新したいときは、ConditionExpression="attribute_exists(キー属性)" を付けます。
Q. 更新後の値を知るために、もう一度 get_item する必要はありますか? A. ReturnValues="UPDATED_NEW"(または ALL_NEW)を付ければ、更新の応答で受け取れます。
まとめ
- UpdateExpression は SET・REMOVE・ADD・DELETE の4つの句。句の間はスペース、句の中はカンマで区切り、同じ句は1回だけ書く
- SET は上書きと追加、加算、list_append、ネストの更新まで担当する。属性がないかもしれないときは if_not_exists を組み合わせる
- ADD は数値とセット、DELETE はセット専用。属性がなくても動く
- ReturnValues で更新前後の値を同じ応答で受け取れる。カウンタには UPDATED_NEW が便利
- 条件なしの update_item は「なければ作る」。既存だけを更新するなら attribute_exists を付ける
- 条件に合わないと ConditionalCheckFailedException。version 属性と組み合わせると楽観ロックになる
- 予約語は # で置き換え、同じ属性(親と子を含む)は1回の更新で1度だけ書く
参考・出典
- Amazon DynamoDB 開発者ガイド「Using update expressions in DynamoDB」(docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html)
- Amazon DynamoDB API Reference「UpdateItem」(docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「DynamoDB UpdateExpressionの書き方|SET・ADD・条件」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。