Java(AWS SDK v2)中的 DynamoDB PutItem

PutItem 写入一整个项目,并替换掉任何具有相同主键的已有项目(基于项目的操作讲了它和 UpdateItem 的区别)。在 AWS SDK for Java 2.x 里,每个属性都以带类型的 AttributeValue 进入 PutItemRequest,而这个 builder 会允许你构造出一个根本不可能合法的值。

代码

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() 能编译。它同时也发不出去。这个 builder 没有任何必填字段,所以一个你忘了写 .s(...) 的属性能完美通过类型检查,然后在服务端失败:

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

这是一个其他 SDK 根本不可能犯的错误在 Java 里的特有形态:Go 的 types.AttributeValueMember* 是彼此独立的类型,所以没有什么东西可以「不设置」。更全面的修法见 "Supplied AttributeValue is empty"

把 Java 的 null 交给 .s(...),不会变成 DynamoDB 的 NULL。真正会咬人的是这个版本,因为它看上去像个值:

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

构造时不会抛 NullPointerException。builder 只是什么都没记录,然后请求以一模一样的 Supplied AttributeValue is empty 消息失败,指向一个你从没怀疑过的属性。如果你要的是真正的 null,那是 AttributeValue.builder().nul(true).build();更多时候你想要的是干脆不放这一项。注意这和 Go SDK 正好相反,在那边一个 nil 指针会被 marshal 成 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() 去比对字面量的代码,都会被一次重试打破,因为重试会改变尝试次数。

DynamoDbException 之前捕获 ConditionalCheckFailedException,并读一读它带了什么。它继承自 DynamoDbException,所以把 catch 块顺序反过来会让专门的处理分支永远走不到。在捕获到的对象上:statusCode() 返回 400retryable() 返回 false,对一次业务逻辑上的拒绝来说这是诚实的答案。给请求加上 .returnValuesOnConditionCheckFailure("ALL_OLD")e.item() 回来时就带着挡住这次写入的那个项目(上面那次运行里是五个属性,YearAttributeValue(N=1994)),于是你不需要再补一次 getItem 去查是谁赢了。

数字通过 .n(...) 以字符串形式传入。DynamoDB 的 N 类型在线上是十进制文本,正是它让 1994 不会变成 double。惯用写法是 .n(String.valueOf(year));没有 .n(int) 这种重载可以用。

这个 client 是 Closeable 的,而且应该长期存活。上面的 try-with-resources 对一次性程序是对的,对服务则是错的:DynamoDbClient 拥有一个 HTTP 连接池而且线程安全,所以整个应用建一个、让它一直活着。每个请求建一个,是这套 API 上最常见的 Java 性能 bug。

更想用 bean 而不是 AttributeValue 映射?那么 DynamoDB Enhanced Clientsoftware.amazon.awssdk.enhanced.dynamodb)会把一个带注解的类直接映射成项目,彻底消掉空 builder 这个陷阱。代价是启动时一次基于反射的 TableSchema.fromBean 扫描,如果你在意这一点,StaticTableSchema 可以避开它。

用可视化的方式来做

上面每一种失败,都是从手工构造带类型的值开始的。免费的 DynamoDB JSON 转换器接收普通 JSON 并返回带类型的形式,于是在写下第一个 AttributeValue.builder() 之前,你就能看清项目在线上到底该长什么样。

要针对你自己的表写入和编辑项目——每个属性一个表单、类型选择器、把结果复制成 Java 代码——请下载 DynoTable

相关示例

参考资料

2026-07-28 以 AWS SDK for Java 2.49.4、OpenJDK 26.0.1,针对 DynamoDB Local(amazon/dynamodb-local,端口 9000)复现。上方的异常文本、状态码和项目内容都是捕获到的输出,原样照录。

无需控制台即可使用 DynamoDB

一款快速的 DynamoDB 桌面客户端,可运行 DynamoDB 无法执行的真正 SQL——JOINs、GROUP BY、聚合——并支持可视化编辑和运行在你自己的 Bedrock 密钥上的 AI agent。

30 天免费试用,无需信用卡 — 之后为无时间限制的免费版。