Java 中的 DynamoDB Query(AWS SDK v2)

AWS SDK for Java 2.x 里的 queryPaginator 看起来像个集合,其实不是。它是一个惰性的可重复迭代对象,而这个差别会在你第二次遍历它的时候体现在账单上。至于 Query 究竟是不是合适的读取方式,参见 Query vs. Scan

代码

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;

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

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

            Map<String, AttributeValue> values = new HashMap<>();
            values.put(":hashKeyValue", AttributeValue.builder().s("Arturo Sandoval").build());
            values.put(":rangeKeyValue", AttributeValue.builder().s("C").build());

            QueryRequest request = QueryRequest.builder()
                    .tableName("Music")
                    .keyConditionExpression(
                            "#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)")
                    .expressionAttributeNames(names)
                    .expressionAttributeValues(values)
                    .build();

            List<Map<String, AttributeValue>> items = new ArrayList<>();
            for (QueryResponse page : ddb.queryPaginator(request)) {
                items.addAll(page.items());
            }
            System.out.println("Found " + items.size() + " items");
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

你每迭代一次,QueryIterable 就重跑一次查询

针对一份 600 首歌的固定数据(每首约 3.9 KB,全部在 Artist = "Arturo Sandoval" 之下),ddb.queryPaginator(request) 在 2.6 毫秒内返回,且一个请求都没发。随后同一个 QueryIterable 对象被遍历了两次:

queryPaginator(request) returned in 2.60 ms   (class QueryIterable, 0 requests)
iteration 1: pages=3  items=600  capacity=284.5
iteration 2: pages=3  items=600  capacity=284.5   <- same object

对一个变量做两个 for 循环,569 个读取单元。页面从未被缓存;每一次迭代都会从头再走一遍 LastEvaluatedKey。如果你需要这些项目两次,就像示例那样,先把这个可迭代对象一次性抽干到一个 List 里。

同一种惰性的另一面,是错误浮出水面的位置。构造一个键条件里漏掉分区键的请求,queryPaginator 会毫无怨言地接受它,因为此刻还没有发生任何调用:

queryPaginator(bad) constructed without throwing
threw on iteration: DynamoDbException / ValidationException /
Query condition missed key schema element / http 400

裹在构建器外面的 try-catch 什么也抓不到。它必须裹住那个循环——这正是示例把整段都放进一个 try 里的原因。

2026-07-28 针对 9000 端口上的 DynamoDB Local(amazon/dynamodb-local),使用 OpenJDK 26.0.1 上的 software.amazon.awssdk:dynamodb 2.49.4 实测。

说明

  • ddb.queryPaginator(request).items() 会把各页拍平成一个 Iterable<Map<String, AttributeValue>> 并在你背后分页,所以当你只想要项目时,示例里的两层循环就塌缩成一层。它也是一个 SdkIterable,因此 .stream() 可用。
  • 在构建器里,数字是 StringAttributeValue.builder().n("1994") 不是 .n(1994) 的笔误——n() 这个 setter 接收的是 java.lang.String,因为 DynamoDB 以十进制文本传输数字,以避开二进制浮点舍入。传一个 Java int 是编译不过的。
  • keyConditionExpression 接收一个分区键上的相等条件,外加至多一个排序键条件(=<<=>>=BETWEENbegins_with);.scanIndexForward(false) 会反转顺序,.indexName("...") 会改为指向一个二级索引。
  • Enhanced Client 是另一套人体工学software.amazon.awssdk.enhanced.dynamodb 映射的是带注解的 bean,而不是 Map<String, AttributeValue>,而它的 query 返回一个 PageIterable<T>,具有上面实测到的同样的惰性重迭代语义。

用可视化的方式来做

本例里的 #hashKey#rangeKey 都是没人逼你用的别名:ArtistSongTitle 都不在 AWS 那份 573 个词的保留字清单上。保留字检查器会告诉你,你的属性名里哪些是真的需要 # 处理的,于是那张别名映射表不再是照猫画虎。

想在动手写 QueryRequest 之前先针对一张真实的表试试键条件,就下载 DynoTable,在网格里翻页浏览结果。

相关示例

参考资料

可视化构建此请求

在免费的 DynamoDB 查询构建器中组装此操作 —— 键条件、筛选、索引、Limit、排序方向和分页循环 —— 再把它作为可运行的 SDK v3、CLI 或 boto3 程序复制回来。

打开 DynamoDB 查询构建器

无需控制台即可使用 DynamoDB

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

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