Java(AWS SDK v2)での DynamoDB PutItem

PutItem はアイテムを丸ごと書き込み、同じプライマリキーを持つ既存のアイテムを置き換えます(UpdateItem との違いはアイテム単位の操作で扱っています)。AWS SDK for Java 2.x では、すべての属性が型付きの AttributeValue として PutItemRequest に入り、そしてビルダーは、どうやっても妥当になりえないものの構築を許してしまいます。

コード

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.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;

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

            Map<String, AttributeValue> item = new HashMap<>();
            item.put("Artist", AttributeValue.builder().s("Arturo Sandoval").build());
            item.put("SongTitle", AttributeValue.builder().s("Cubano Chant").build());
            item.put("AlbumTitle", AttributeValue.builder().s("Danzon").build());
            item.put("Year", AttributeValue.builder().n("1994").build());
            item.put("Awards", AttributeValue.builder().n("0").build());

            Map<String, String> names = new HashMap<>();
            names.put("#cond0", "Artist");
            names.put("#cond1", "SongTitle");

            PutItemRequest request = PutItemRequest.builder()
                    .tableName("Music")
                    .item(item)
                    .conditionExpression("attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)")
                    .expressionAttributeNames(names)
                    .build();

            try {
                ddb.putItem(request);
                System.out.println("Song written");
            } catch (ConditionalCheckFailedException e) {
                System.out.println("A song with that key already exists — not overwritten");
            }
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

解説

AttributeValue.builder().build() はコンパイルが通ります。そして送信できません。ビルダーに必須フィールドは無いので、.s(...) を忘れた属性は型チェックを完璧に通過し、サービス側で失敗します。

DynamoDbException | ValidationException | Supplied AttributeValue is empty, must contain exactly one of the supported datatypes

これは、他の SDK では起こしようがない誤りの Java 固有の形です。Go の types.AttributeValueMember* は別々の型なので、未設定にしておけるものがありません。より広い対処法は"Supplied AttributeValue is empty"を参照してください。

.s(...) に渡した Java の null は、DynamoDB の NULL にはなりません。実際に刺さるのはこちらのバージョンです。値のように見えるからです。

AttributeValue.builder().s(customer.getNotes()).build()   // getNotes() returned null

構築時に NullPointerException は送出されません。ビルダーは単に何も記録せず、リクエストはまったく同じ Supplied AttributeValue is empty というメッセージで失敗し、疑ってもいなかった属性を指し示します。本物の null が欲しいなら AttributeValue.builder().nul(true).build() です。もっと多いのは、そのエントリ自体を省きたい場合でしょう。これは Go SDK とは逆である点に注意してください。あちらでは nil ポインタが NULL にマーシャルされ、黙って属性を作ります。両者は同じ日に同じエンジンに対して実行しました。

getMessage() はサービスのメッセージではありません。SDK が独自のコンテキストを付け足すので、文字列はこうなります。

The conditional request failed (Service: DynamoDb, Status Code: 400, Request ID: 77a08ef1-a3a9-4f97-b309-4cb0741edd1a) (SDK Attempt Count: 1)

e.awsErrorDetails().errorCode() でマッチし、素のテキストは e.awsErrorDetails().errorMessage() で読みましょう。getMessage() をリテラルと比較するものは、試行回数を変えるリトライ 1 回で壊れます。

ConditionalCheckFailedExceptionDynamoDbException より先に捕捉し、それが携えているものを読みましょう。これは DynamoDbException を継承しているので、catch ブロックを逆順に並べると個別のハンドラーに到達できなくなります。捕捉したオブジェクトでは、statusCode()400 を、retryable()false を返しました。ビジネスロジック上の拒否に対する正直な答えです。リクエストに .returnValuesOnConditionCheckFailure("ALL_OLD") を足すと、e.item() が書き込みを阻んだアイテムを中身入りで返します(上の実行では属性 5 つ、YearAttributeValue(N=1994))。誰が勝ったのかを知るために追加の getItem を打つ必要はありません。

数値は .n(...) を通して文字列として入ります。DynamoDB の N 型はワイヤ上では 10 進のテキストであり、それが 1994 を double にさせない仕組みです。イディオムは .n(String.valueOf(year)) で、手を伸ばせる .n(int) のオーバーロードはありません。

クライアントは Closeable で、かつ長命です。上の try-with-resources は一発ものプログラムには正しく、サービスには誤りです。DynamoDbClient は HTTP コネクションプールを所有し、スレッドセーフなので、アプリケーションごとに 1 つ作って生かしておきましょう。リクエストごとに作るのが、この API で最もよくある Java のパフォーマンスバグです。

AttributeValue のマップより Bean を選びたい場合DynamoDB Enhanced Clientsoftware.amazon.awssdk.enhanced.dynamodb)は、アノテーションを付けたクラスをアイテムへ直接マッピングし、空ビルダーの罠を丸ごと取り除きます。代償は起動時のリフレクションによる TableSchema.fromBean のスキャンで、それが気になるなら StaticTableSchema で回避できます。

ビジュアルに行う

上のどの失敗も、型付きの値を手で組み立てるところから始まります。無料の DynamoDB JSON コンバーターは、普通の JSON を受け取って型付きの形を返すので、AttributeValue.builder() を 1 行も書く前に、アイテムがワイヤ上でどう見えるべきかを正確に確認できます。

自分のテーブルに対してアイテムを書き込み・編集するには — 属性ごとのフォーム、型ピッカー、結果の Java としてのコピー — DynoTable をダウンロードしてください。

関連する例

参考資料

2026-07-28 に、OpenJDK 26.0.1 上の AWS SDK for Java 2.49.4 で、ポート 9000 の DynamoDB Local(amazon/dynamodb-local)に対して再現しました。上記の例外テキスト、ステータスコード、アイテムの内容は、取得した出力をそのまま逐語で写したものです。

Console なしで DynamoDB を扱う

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

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