Java (AWS SDK v2) での DynamoDB UpdateItem

更新そのものはビルダーの呼び出し 1 回です。Java 開発者の時間を奪うのはその周りのすべてです。決して null を返さないレスポンスオブジェクト、いちばん知りたい失敗が、おそらくあなたが捕捉した型のサブクラスになっている例外階層、そしてこの操作をそもそも表現できない高レベルのクライアント。

コード

import java.util.HashMap;
import java.util.Map;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;

public class UpdateItemExample {
    public static void main(String[] args) {
        try (DynamoDbClient ddb = DynamoDbClient.builder()
                .region(Region.US_EAST_1)
                .build()) {

            Map<String, AttributeValue> key = new HashMap<>();
            key.put("Artist", AttributeValue.builder().s("Arturo Sandoval").build());
            key.put("SongTitle", AttributeValue.builder().s("Cubano Chant").build());

            Map<String, String> names = new HashMap<>();
            names.put("#upd0", "Genre");
            names.put("#upd1", "Year");
            names.put("#upd2", "Awards");

            Map<String, AttributeValue> values = new HashMap<>();
            values.put(":updValue0", AttributeValue.builder().s("Latin Jazz").build());
            values.put(":updValue1", AttributeValue.builder().n("1994").build());
            values.put(":updValue2", AttributeValue.builder().n("1").build());

            UpdateItemRequest request = UpdateItemRequest.builder()
                    .tableName("Music")
                    .key(key)
                    .updateExpression("SET #upd0 = :updValue0, #upd1 = :updValue1 ADD #upd2 :updValue2")
                    .expressionAttributeNames(names)
                    .expressionAttributeValues(values)
                    .returnValues(ReturnValue.ALL_NEW)
                    .build();

            UpdateItemResponse response = ddb.updateItem(request);
            System.out.println(response.attributes()); // the item after the update
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

解説

  • AttributeValue.builder().n("1994")String を取ります。短い方の AttributeValue.fromN("1994") も同じです。n(int) のオーバーロードはありません。DynamoDB の数値は 38 桁の有効数字を保持しますが、Java のどのプリミティブもそうではないからです。読み戻すときも attributes().get("Awards").n()String です。型違いのアクセサーは例外を投げずに null を返すので、数値に対する .s() は静かな null になります。どれが設定されているかは .type() が教えてくれます。

  • response.attributes() は決して null を返しませんReturnValue.NONE のときは、空だが非 null の DefaultSdkAutoConstructMap を返すので、null チェックは決して発火せず、isEmpty() のチェックでも「サービスが何も送らなかった」と「アイテムに属性がない」を区別できません。その違いを知っているアクセサーが、生成された hasAttributes() です。この SDK のコレクションメンバーにはすべて 1 つずつ用意されています。

  • ビルダーは、肝心の部分以外のすべてを型チェックしますupdateExpression(String) は任意の文字列を受け付けます。コンパイラーは SET とタイプミスを区別できないので、式の間違いは実行時の 400 になります。ADD #upd2 :updValue2 がアトミックなインクリメントで、attribute_exists(Artist)conditionExpression を付ければ呼び出しは更新のみになります。文法は更新式にあります。

  • 旧来の attributeUpdates マップより式を優先しましょう。古い例には今も出てきますが、複数種類の句も、エイリアスも、1 リクエストの中の条件も表現できません。

  • getMessage() はサービスのメッセージではありません。SDK が独自のトランスポート情報を末尾に足します。

    The conditional request failed (Service: DynamoDb, Status Code: 400, Request ID: d99b117c-edd6-4dc9-8d3a-a5fa4fe9666c) (SDK Attempt Count: 1)

    サポートケース用にリクエスト ID が欲しいなら、これをログに出しましょう。比較は代わりに awsErrorDetails().errorCode() で行い、素の文字列が欲しいときは awsErrorDetails().errorMessage() を使います。

ここでは catch の順序がいつも以上に重要

ConditionalCheckFailedException extends DynamoDbException なので、catch (DynamoDbException e) を先に置くと、ほぼ間違いなく分岐したかったその失敗を飲み込んでしまいます。具体的な型を先に捕まえ、ついでにアイテムも受け取りましょう。

} catch (ConditionalCheckFailedException e) {
    // with .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
    if (e.hasItem()) {
        Map<String, AttributeValue> loser = e.item();  // the item as it actually was
    }
} catch (DynamoDbException e) {
    // everything else
}

これについては e.retryable()false で、それが正しい挙動です。失敗した条件をリトライしても、また失敗するだけです。

覚えておくべき非対称性は、この SDK に ValidationException のクラスが存在しない ことです。dynamodb-2.35.9.jar を探しても、捕捉できるものはありません。予約語、不正な式、部分的なキー — そのすべてが、awsErrorDetails().errorCode() がたまたま ValidationException と読める、ただの DynamoDbException として届きます。静的型付け言語ではこれは面食らう欠落であり、式の間違いが実行時の文字列比較になることを意味します。

だからこそ DynamoDB の予約語は、出荷した後ではなく前に一度目を通す価値があります。リストは 573 項目に及び、YearNameStatus を含みますが、どれも Java の Bean の中では危なそうに見えません。Bean のマッピング越しではなく生のテーブルを閲覧するには、DynoTable をダウンロードしてください。

Enhanced クライアントはこれを表現できない

データアクセスの残りが DynamoDbEnhancedClient とアノテーション付きの Bean を通っているなら、この操作こそ DynamoDbClient に引き戻される 1 つです。UpdateItemEnhancedRequest.Builder をリフレクションで覗くと、itemconditionExpressionignoreNullsignoreNullsModereturnValuesreturnValuesOnConditionCheckFailurereturnConsumedCapacityreturnItemCollectionMetrics が見つかります。更新式を受け取るメソッドはありません。

実務上の帰結はアトミックカウンターです。ADD #upd2 :updValue2 は先に読み取ることなくサーバー側で Awards をインクリメントします。Enhanced クライアントが与えてくれるのは、マッピングされた Bean と、欠けたフィールドを削除するかどうかを決める ignoreNulls であって、ADD にコンパイルされるものは何もありません。Bean を介した read-modify-write は、並行実行下では更新消失のレースです。このページのスニペットが避けているのは、まさにそれです。

関連する例

参考資料

最終検証日 2026-07-28、上記にリンクした公式 AWS ドキュメントに照らして確認しました。

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。