1. 简介

Amazon DynamoDB 是AWS提供的核心服务之一,广泛应用于构建快速、可扩展且无服务器的应用程序。它提供完全托管的NoSQL数据库解决方案,在任何规模下都能实现毫秒级性能。与传统关系型数据库不同,DynamoDB采用键值和文档数据模型,强调在设计阶段就规划好数据访问模式。

本教程将聚焦DynamoDB最强大的特性之一:使用组合主键(分区键+排序键)进行数据查询。我们将深入解析组合键模型的工作原理,并通过AWS SDK for Java演示如何高效执行这类查询。

2. 理解组合键模型

DynamoDB支持两种主键类型:简单主键(仅分区键)和组合主键(分区键+排序键)

  • 分区键决定数据的存储位置
  • 排序键允许在分区内进行排序和过滤

这种模型特别适合将相关数据(如用户订单)组织在单一键下。以UserOrders表为例:

  • userId作为分区键
  • orderDate作为排序键

✅ 这种设计能轻松实现:

  • 获取用户所有订单
  • 按日期排序
  • 按时间范围过滤
  • 全部通过单次查询完成

3. Maven依赖

使用Java操作DynamoDB需要引入AWS SDK for Java v2,它提供现代的非阻塞API:

<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>dynamodb</artifactId>
    <version>2.31.26</version>
</dependency>

该依赖提供了DynamoDbClient等核心类,用于执行表查询操作。

4. 基于分区键的查询

最简单也最常用的DynamoDB查询方式就是使用分区键。当按分区键查询时,DynamoDB会返回所有具有相同分区键值的项目。

假设UserOrders表存储多个用户的订单,现在要获取单个用户的所有订单。由于userId是分区键,可以这样实现:

QueryRequest queryRequest = QueryRequest.builder()
  .tableName("UserOrders")
  .keyConditionExpression("userId = :uid")
  .expressionAttributeValues(Map.of(
    ":uid", AttributeValue.builder().s("user1").build()
  )).build();

QueryResponse response = dynamoDbClient.query(queryRequest);

⚠️ 注意:

  • 使用构建器模式构造请求,便于处理复杂场景
  • 查询结果封装在QueryResponse对象中

获取实际数据需要调用items()方法:

List<Map<String, AttributeValue>> items = response.items();

for (Map<String, AttributeValue> item : items) {
    System.out.println("Order item: " + item.get("item").s());
}

返回的列表中每个项目都是属性名到值的映射,可进一步处理或转换为业务对象。

5. 基于分区键和排序键的查询

仅用分区键查询有时不够精确,我们可以结合排序键条件实现更精细的过滤。这允许在分区内按日期范围或特定前缀筛选结果。

例如要获取user1在2025年1月1日之后的所有订单,由于orderDate是排序键,可以在keyConditionExpression中添加比较条件:

QueryRequest queryRequest = QueryRequest.builder()
  .tableName("UserOrders")
  .keyConditionExpression("userId = :uid AND orderDate > :startDate")
  .expressionAttributeValues(Map.of(
    ":uid", AttributeValue.builder().s("user1").build(),
    ":startDate", AttributeValue.builder().s("2025-01-01").build()
  )).build();

QueryResponse response = dynamoDbClient.query(queryRequest);

✅ 这种方式的优势:

  • 仅扫描user1的分区
  • 只返回orderDate大于2025-01-01的项目
  • 避免扫描无关数据,效率极高

6. 常用排序键条件

DynamoDB支持多种排序键过滤操作符,这些操作符让我们能在分区内使用标准比较逻辑精确筛选结果。

6.1. BETWEEN操作符

使用BETWEEN可获取特定范围内的项目,特别适合处理时间戳或日期:

QueryRequest queryRequest = QueryRequest.builder()
  .tableName("UserOrders")
  .keyConditionExpression("userId = :uid AND orderDate BETWEEN :from AND :to")
  .expressionAttributeValues(Map.of(
    ":uid", AttributeValue.builder().s("user1").build(),
    ":from", AttributeValue.builder().s("2024-12-01").build(),
    ":to", AttributeValue.builder().s("2024-12-31").build()
  )).build();

这将返回user1在2024年12月的所有订单。

6.2. BEGINS_WITH操作符

当排序键是字符串(如格式化日期)时,可用BEGINS_WITH查询特定前缀的项目,适合按年、月等分组:

QueryRequest queryRequest = QueryRequest.builder()
  .tableName("UserOrders")
  .keyConditionExpression("userId = :uid AND begins_with(orderDate, :prefix)")
  .expressionAttributeValues(Map.of(
    ":uid", AttributeValue.builder().s("user1").build(),
    ":prefix", AttributeValue.builder().s("2025-01").build()
  )).build();

这将返回user1在2025年1月的所有订单。

7. 处理查询分页

DynamoDB限制单次查询响应最大为1MB数据。当查询结果超过此限制时,响应中会包含LastEvaluatedKey,用于获取下一页结果。

需要通过循环处理分页:

List<Map<String, AttributeValue>> allItems = new ArrayList<>();
Map<String, AttributeValue> lastKey = null;

do {
    QueryRequest.Builder requestBuilder = QueryRequest.builder()
      .tableName("UserOrders")
      .keyConditionExpression("userId = :uid")
      .expressionAttributeValues(Map.of(
        ":uid", AttributeValue.fromS(userId)
      ));

    if (lastKey != null) {
        requestBuilder.exclusiveStartKey(lastKey);
    }

    QueryResponse response = dynamoDb.query(requestBuilder.build());
    allItems.addAll(response.items());
    lastKey = response.lastEvaluatedKey();
} while (lastKey != null && !lastKey.isEmpty());

return allItems;

✅ 这种模式确保:

  • 获取所有匹配项目
  • 不受单页数据量限制
  • 特别适合大数据量分区查询或报表导出

8. 总结

DynamoDB的组合键模型为我们提供了高效组织和检索数据的强大能力。分区键与排序键的结合,使我们能够:

  • 组织相关数据
  • 创建高效、可扩展的访问模式
  • 满足高性能应用需求

本文探讨了:

  • 仅使用分区键的查询方法
  • 结合排序键过滤分区内结果的增强查询

完整代码示例可在GitHub获取。