Java(AWS SDK v2)中的 DynamoDB UpdateItem

更新本身就是一次构建器调用。真正耗掉 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 里每个集合成员都有一个。

  • 构建器什么都做类型检查,唯独最要紧的那部分不检查updateExpression(String) 接受任意字符串;编译器分不清 SET 和一个笔误,所以表达式错误都是运行时的 400。ADD #upd2 :updValue2 是那次原子自增,一个 attribute_exists(Artist)conditionExpression 会让这次调用只做更新,而语法在更新表达式里。

  • 优先用表达式,而不是老式的 attributeUpdates 映射。老一些的示例还在展示它;它无法在一个请求里表达多种子句类型、别名或条件。

  • 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 里搜一遍,没有任何东西可以 catch。保留字、格式错误的表达式、不完整的键:它们全都以一个普通的 DynamoDbException 的形式到达,只是它的 awsErrorDetails().errorCode() 恰好读出来是 ValidationException。在一门静态类型语言里,这是个突兀的缺口,也意味着表达式错误变成了运行时的字符串比较。

这正是为什么 DynamoDB 的保留字值得在发版之前而不是之后过一遍:那份列表有 573 条,其中包括 YearNameStatus,在一个 Java bean 里它们看着都毫无危险。要浏览原始的表、而不是它上面那层 bean 映射,请下载 DynoTable

增强客户端表达不了这个操作

如果你其余的数据访问都走 DynamoDbEnhancedClient 和带注解的 bean,那么这个操作就是把你打回 DynamoDbClient 的那一个。对 UpdateItemEnhancedRequest.Builder 做反射,能找到 itemconditionExpressionignoreNullsignoreNullsModereturnValuesreturnValuesOnConditionCheckFailurereturnConsumedCapacityreturnItemCollectionMetrics。没有任何一个方法接收更新表达式。

实际后果就是那个原子计数器。ADD #upd2 :updValue2 在服务端给 Awards 自增,事先不需要读;增强客户端给你的是一个映射好的 bean,外加一个决定缺失字段是否被删除的 ignoreNulls,却没有任何能编译成 ADD 的东西。通过 bean 做"读—改—写"在并发下就是一场丢失更新的竞态,而这恰恰是本页这段代码所避开的。

相关示例

参考资料

最后核实于 2026-07-28,依据上方链接的 AWS 官方文档。

无需控制台即可使用 DynamoDB

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

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