news 2026/9/18 8:15:13

AWS SDK for Java v2 深度解析:DynamoDB Enhanced Client 的对象映射、扩展机制与异步操作

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AWS SDK for Java v2 深度解析:DynamoDB Enhanced Client 的对象映射、扩展机制与异步操作

AWS SDK for Java v2 深度解析:DynamoDB Enhanced Client 的对象映射、扩展机制与异步操作

【免费下载链接】aws-sdk-java-v2The official AWS SDK for Java - Version 2项目地址: https://gitcode.com/GitHub_Trending/aw/aws-sdk-java-v2

本文基于 DynamoDB Enhanced Client 官方文档 系统讲解 AWS SDK for Java v2 中 DynamoDB 中间层客户端的完整用法:如何定义 Bean/不可变类与 TableSchema、执行 CRUD 与批量事务操作、查询二级索引、使用异步非阻塞接口,以及如何通过beforeWrite/afterRead扩展点实现乐观锁、原子计数器与自动时间戳。读完本文,你可以直接在 Java 项目中使用对象化的 DynamoDB 编程模型,并结合仓库源码理解其底层映射与扩展机制。

模块定位:什么是 DynamoDB Enhanced Client

DynamoDB Enhanced Client 是构建在低层DynamoDbClient之上的中间层(mid-level)映射/抽象层,位于仓库的services-custom/dynamodb-enhanced模块。从 模块 POM 可以看到:

  • Maven 坐标为software.amazon.awssdk:dynamodb-enhanced,依赖同版本的dynamodb服务客户端、sdk-coreaws-core等核心模块;
  • 基线运行时为 JRE 1.8,JAR 的Automatic-Module-Namesoftware.amazon.awssdk.enhanced.dynamodb
  • 测试体系使用 DynamoDB Local(com.amazonaws:DynamoDBLocal)+ WireMock 做功能级验证。

其核心抽象在 TableSchema.java:一个能把 Java 对象与Map<String, AttributeValue>相互映射、并持有表结构元数据(分区键、排序键、二级索引)的映射器。

快速上手:从 Bean 定义到 CRUD

1. 定义 DynamoDb Bean

重要前提@DynamoDbBean类的字段绝不能声明为final。Enhanced Client 要求字段可变(mutable)才能正确完成映射。

以下示例使用一个虚构的Customer类(非库内置类),键值均为任意取值:

@DynamoDbBean public class Customer { private String accountId; private int subId; // primitive types are supported private String name; private Instant createdDate; @DynamoDbPartitionKey public String getAccountId() { return this.accountId; } public void setAccountId(String accountId) { this.accountId = accountId; } @DynamoDbSortKey public int getSubId() { return this.subId; } public void setSubId(int subId) { this.subId = subId; } // Defines a GSI (customers_by_name) with a partition key of 'name' @DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name") public String getName() { return this.name; } public void setName(String name) { this.name = name; } // Defines an LSI (customers_by_date) with a sort key of 'createdDate' and also declares the // same attribute as a sort key for the GSI named 'customers_by_name' @DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"}) public Instant getCreatedDate() { return this.createdDate; } public void setCreatedDate(Instant createdDate) { this.createdDate = createdDate; } }

注解均定义在 mapper/annotations 包下,如 DynamoDbBean.java、DynamoDbPartitionKey.java、DynamoDbSecondarySortKey.java。

2. 构建 TableSchema:注解推断 vs 静态声明

最简方式是使用TableSchema.fromClass(),它会扫描注解并推断表结构(源码中fromClass位于 TableSchema.java#L203):

static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.fromClass(Customer.class);

如果不想付出 Bean 推断的开销,可以用StaticTableSchema的 builder 直接声明 schema,此时类不需要遵循 Bean 命名规范、也无需任何注解。下面这个示例与上面的 Bean 示例完全等价:

static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("account_id") .getter(Customer::getAccountId) .setter(Customer::setAccountId) .tags(primaryPartitionKey())) .addAttribute(Integer.class, a -> a.name("sub_id") .getter(Customer::getSubId) .setter(Customer::setSubId) .tags(primarySortKey())) .addAttribute(String.class, a -> a.name("name") .getter(Customer::getName) .setter(Customer::setName) .tags(secondaryPartitionKey("customers_by_name"))) .addAttribute(Instant.class, a -> a.name("created_date") .getter(Customer::getCreatedDate) .setter(Customer::setCreatedDate) .tags(secondarySortKey("customers_by_date"), secondarySortKey("customers_by_name"))) .build();

TableSchema.builder(...)的静态工厂在接口中直接委托给 StaticTableSchema,而 TableSchema.java#L45-L46 上的@ThreadSafe标注说明 schema 一旦构建完成即可跨线程复用。Javadoc 明确建议:由于反射推断是"适度昂贵"的操作,每个类只应创建一次 schema,通常在应用启动时完成。

3. 创建 Enhanced Client 与 Table 资源

// 创建增强客户端,用于对多张表重复执行操作 DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(dynamoDbClient) .build(); // 将物理表 'customers_20190205' 映射到 schema DynamoDbTable<Customer> customerTable = enhancedClient.table("customers_20190205", CUSTOMER_TABLE_SCHEMA);

table()传入的名称若表已存在,必须与实际 DynamoDB 表名一致;若表尚不存在,该名称会在随后的createTable()中被用作新表名。接口定义见 DynamoDbTable.java 与 DynamoDbEnhancedClient.java。

常用基础操作:与底层原语一一对应

以下操作强映射到同名的 DynamoDB 原语。示例给出的是最简形态,每个操作都可以通过传入增强请求对象(Enhanced Request)进一步定制,这些请求对象提供了低层 DynamoDB 客户端的绝大部分能力,完整文档见相应接口的 Javadoc:

// CreateTable customerTable.createTable(); // GetItem Customer customer = customerTable.getItem(Key.builder().partitionValue("a123").build()); // UpdateItem Customer updatedCustomer = customerTable.updateItem(customer); // PutItem customerTable.putItem(customer); // DeleteItem Customer deletedCustomer = customerTable.deleteItem(Key.builder().partitionValue("a123").sortValue(456).build()); // Query PageIterable<Customer> customers = customerTable.query(keyEqualTo(k -> k.partitionValue("a123"))); // Scan PageIterable<Customer> customers = customerTable.scan(); // BatchGetItem BatchGetResultPageIterable batchResults = enhancedClient.batchGetItem(r -> r.addReadBatch(ReadBatch.builder(Customer.class) .mappedTableResource(customerTable) .addGetItem(key1) .addGetItem(key2) .addGetItem(key3) .build())); // BatchWriteItem batchResults = enhancedClient.batchWriteItem(r -> r.addWriteBatch(WriteBatch.builder(Customer.class) .mappedTableResource(customerTable) .addPutItem(customer) .addDeleteItem(key1) .addDeleteItem(key1) .build())); // TransactGetItems transactResults = enhancedClient.transactGetItems(r -> r.addGetItem(customerTable, key1) .addGetItem(customerTable, key2)); // TransactWriteItems enhancedClient.transactWriteItems(r -> r.addConditionCheck(customerTable, i -> i.key(orderKey) .conditionExpression(conditionExpression)) .addUpdateItem(customerTable, customer) .addDeleteItem(customerTable, key));

这些操作在internal/operations包中各有对应的操作实现(如 GetItemOperation.java、QueryOperation.java),批量与分页结果模型位于 model 包(ReadBatchWriteBatchPageIterableBatchGetResultPageIterable等)。

在二级索引上执行 Query 与 Scan

Query 和 Scan 可以针对二级索引执行,示例如下:

DynamoDbIndex<Customer> customersByName = customerTable.index("customers_by_name"); SdkIterable<Page<Customer>> customersWithName = customersByName.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("Smith")))); PageIterable<Customer> pages = PageIterable.create(customersWithName);

索引对象由 DynamoDbIndex.java 定义;查询条件由internal/conditional包下的SingleKeyItemConditionalBetweenConditionalBeginsWithConditional等类型组合而成,测试用例见 QueryConditionalUtilsTest.java。

与不可变数据类协作

Enhanced Client 可以直接映射不可变类:不可变类只有 getter,并配有一个独立的 Builder 类用于构造实例。注解风格与 Bean 类似,核心注解为@DynamoDbImmutable(builder = ...)

@DynamoDbImmutable(builder = Customer.Builder.class) public class Customer { private final String accountId; private final int subId; private final String name; private final Instant createdDate; private Customer(Builder b) { this.accountId = b.accountId; this.subId = b.subId; this.name = b.name; this.createdDate = b.createdDate; } // This method will be automatically discovered and used by the TableSchema public static Builder builder() { return new Builder(); } @DynamoDbPartitionKey public String accountId() { return this.accountId; } @DynamoDbSortKey public int subId() { return this.subId; } @DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name") public String name() { return this.name; } @DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"}) public Instant createdDate() { return this.createdDate; } public static final class Builder { private String accountId; private int subId; private String name; private Instant createdDate; private Builder() {} public Builder accountId(String accountId) { this.accountId = accountId; return this; } public Builder subId(int subId) { this.subId = subId; return this; } public Builder name(String name) { this.name = name; return this; } public Builder createdDate(Instant createdDate) { this.createdDate = createdDate; return this; } // This method will be automatically discovered and used by the TableSchema public Customer build() { return new Customer(this); } } }

@DynamoDbImmutable类必须满足以下约定(这些校验由 ImmutableIntrospector.java 在 schema 推断阶段执行):

  1. 不可变类上除Object重写方法、或标注了@DynamoDbIgnore的方法外,每个方法都必须是记录属性的 getter;
  2. 每个 getter 必须在 builder 类上有大小写匹配的对应 setter;
  3. builder 类必须有公共无参默认构造函数;或者不可变类上必须有一个名为builder的公共静态无参方法,返回 builder 实例;
  4. builder 类必须有一个名为build的公共无参方法,返回不可变类实例。

为不可变类创建 schema 时,使用专门的静态构造方法:

static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.fromImmutableClass(Customer.class);

fromImmutableClass的实现入口在 TableSchema.java#L164-L166,返回 ImmutableTableSchema。

如果项目使用 Lombok 等第三方库生成样板代码,只要遵循上述约定即可兼容。借助 Lombok 的onMethod特性,可以把基于属性的 DynamoDb 注解复制到生成的 getter 上:

@Value @Builder @DynamoDbImmutable(builder = Customer.CustomerBuilder.class) public static class Customer { @Getter(onMethod = @__({@DynamoDbPartitionKey})) private String accountId; @Getter(onMethod = @__({@DynamoDbSortKey})) private int subId; @Getter(onMethod = @__({@DynamoDbSecondaryPartitionKey(indexNames = "customers_by_name")})) private String name; @Getter(onMethod = @__({@DynamoDbSecondarySortKey(indexNames = {"customers_by_date", "customers_by_name"})})) private Instant createdDate; }

非阻塞异步操作

如果应用需要非阻塞调用,可以使用映射器的异步实现。它与同步版本非常相似,但有三个关键差异:

  1. 实例化映射表时使用异步版本,并搭配 SDK 的异步 DynamoDb 客户端:
DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() .dynamoDbClient(dynamoDbAsyncClient) .build();
  1. 返回单个数据的操作返回CompletableFuture,可以在等待结果期间执行其他工作:
CompletableFuture<Customer> result = mappedTable.getItem(r -> r.key(customerKey)); // Perform other work here return result.join(); // now block and wait for the result
  1. 分页列表操作返回SdkPublisher(而非SdkIterable),可以订阅一个处理器异步处理结果:
PagePublisher<Customer> results = mappedTable.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("Smith")))); results.subscribe(myCustomerResultsProcessor); // Perform other work and let the processor handle the results asynchronously

异步客户端与异步表/索引接口分别见 DynamoDbEnhancedAsyncClient.java、DynamoDbAsyncTable.java,分页发布模型为 PagePublisher.java。异步路径的功能测试位于functionaltests目录(如 AsyncBasicCrudTest.java、AsyncBasicQueryTest.java)。

扩展机制:beforeWrite 与 afterRead 两个钩子

映射器支持插件式扩展,提供两个钩子:beforeWrite()在写入发生前被调用(可以改写写操作),afterRead()在读取发生后被调用(可以改写读结果)。像 UpdateItem 这类"先写后读"的操作会同时触发两个钩子。接口定义见 DynamoDbEnhancedClientExtension.java#L43-L57,钩子的上下文与返回值模型为 WriteModification.java 和 ReadModification.java。

加载顺序很重要:扩展按在 enhanced client builder 中声明的顺序加载,因为后一个扩展可能作用于前一个扩展变换后的值。从源码看,ExtensionResolver.java#L32-L38 确认了默认扩展列表只包含两个:VersionedRecordExtensionAtomicCounterExtension;多个扩展会通过内部的ChainExtension元扩展按严格顺序串联(resolveExtensions方法,同文件 #L61-L71)。默认行为可以在 client builder 上覆盖——加载任意自定义扩展,或者一个都不加载。

示例:在默认加载的VersionedRecordExtension之后再加载一个自定义扩展verifyChecksumExtension

DynamoDbEnhancedClientExtension versionedRecordExtension = VersionedRecordExtension.builder().build(); DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(dynamoDbClient) .extensions(versionedRecordExtension, verifyChecksumExtension) .build();

VersionedRecordExtension:乐观锁

该扩展默认加载,会为记录维护一个版本号,并在每次写入时自动递增。它会给每个写操作附加条件表达式:如果数据库中记录的版本号与应用上次读取的值不一致,写入即失败。若其他进程在"第一进程读取记录"与"第一进程写回更新"之间更新了该记录,这次写入就会失败——这实际上为记录更新提供了乐观锁(optimistic locking)。

告诉扩展用哪个属性记录版本号,在 TableSchema 中标注一个数值属性:

@DynamoDbVersionAttribute public Integer getVersion() {...}; public void setVersion(Integer version) {...};

或者使用 StaticTableSchema 的标签:

.addAttribute(Integer.class, a -> a.name("version") .getter(Customer::getVersion) .setter(Customer::setVersion) // Apply the 'version' tag to the attribute .tags(versionAttribute()))

从 VersionedRecordExtension.java 的实现可以看到关键细节:

  • 版本号属性必须是N(数值)类型,否则在 schema 构建期抛IllegalArgumentException(#L123-L127);
  • 版本计算:新记录首次写入的版本号为startAt + incrementBy,默认startAt=0incrementBy=1,因此第一版为 1;若希望从 0 开始编号,可设startAt=-1(#L58-L60);
  • beforeWrite(#L145 起)中新记录使用attribute_not_exists(...)条件,已有记录则生成版本号 = 上次读取值的条件表达式,且当版本号恰好等于startAt时使用OR条件兼容"新建/已存在"两种情形;还带有Long.MAX_VALUE溢出保护(#L191-L195)。

AtomicCounterExtension:原子计数器

该扩展默认加载,每次记录写入时自动递增数值属性;起始值和步长均可指定,未指定时计数器从 0 开始、每次加 1。

在 TableSchema 中将一个Long属性标记为计数器(标准值示例):

@DynamoDbAtomicCounter public Long getCounter() {...}; public void setCounter(Long counter) {...};

使用 StaticTableSchema 并指定自定义起始值与步长:

.addAttribute(Integer.class, a -> a.name("counter") .getter(Customer::getCounter) .setter(Customer::setCounter) // Apply the 'atomicCounter' tag to the attribute with start and increment values .tags(atomicCounter(10L, 5L)))

AtomicCounterExtension.java 的 Javadoc 与实现(beforeWrite,#L113-L135)确认了两个关键行为:

  • putItem 时计数器会被重置为起始值addToItem,#L143-L147);
  • updateItem 时扩展会从待写记录中剥离计数器属性,并通过 UpdateExpression 生成if_not_exists(计数器, 起始值) + 增量的原子自增表达式;手动修改被标记为原子计数器的属性不会生效filterFromItem,#L149-L163)。

AutoGeneratedTimestampRecordExtension:自动时间戳

该扩展使被选中的属性在每次成功写入时自动更新为当前时间戳。要求属性类型为Instant

与上面两个扩展不同,它默认不加载,必须在创建 enhanced client 时作为自定义扩展显式指定。通过标注Instant属性告诉扩展要更新哪个属性:

@DynamoDbAutoGeneratedTimestampAttribute public Instant getLastUpdate() {...} public void setLastUpdate(Instant lastUpdate) {...}

StaticTableSchema 写法:

.addAttribute(Instant.class, a -> a.name("lastUpdate") .getter(Customer::getLastUpdate) .setter(Customer::setLastUpdate) // Applying the 'autoGeneratedTimestamp' tag to the attribute .tags(autoGeneratedTimestampAttribute()))

实现见 AutoGeneratedTimestampRecordExtension.java,功能测试见 AutoGeneratedTimestampExtensionTest.java。此外,从 extensions/annotations 包可以看到,仓库中还提供了DynamoDbAutoGeneratedUuid(对应AutoGeneratedUuidExtension)这一扩展注解,用于自动生成 UUID 属性。

高级 TableSchema 特性

显式包含/排除属性

排除属性:用@DynamoDbIgnore标记不参与映射的属性:

private String internalKey; @DynamoDbIgnore public String getInternalKey() { return this.internalKey; } public void setInternalKey(String internalKey) { return this.internalKey = internalKey;}

包含并重命名属性:用@DynamoDbAttribute("名字")显式指定存储时使用的属性名:

private String internalKey; @DynamoDbAttribute("renamedInternalKey") public String getInternalKey() { return this.internalKey; } public void setInternalKey(String internalKey) { return this.internalKey = internalKey;}

两个注解分别位于 DynamoDbIgnore.java 与 DynamoDbAttribute.java。

控制属性转换(Attribute Converter)

默认情况下,TableSchema 通过 DefaultAttributeConverterProvider.java 为所有基本类型和许多常见 Java 类型提供转换器。行为既可以在"转换器提供者"层面整体调整,也可以针对单个属性覆盖。可用转换器的完整清单可参考 AttributeConverter.java 接口的 Javadoc;从 internal/converter/attribute 包可见内置实现覆盖面很广,包括InstantAsStringAttributeConverterLocalDateAttributeConverterMapAttributeConverterListAttributeConverterSetAttributeConverterSdkBytesAttributeConverterUriAttributeConverterUrlAttributeConverterUuidAttributeConverterEnumAttributeConverter(位于包根目录)等。

提供自定义转换器提供者

可以通过@DynamoDbBeanconverterProviders注解提供一个或一整个有序链。自定义实现必须继承AttributeConverterProvider接口(AttributeConverterProvider.java)。注意:提供自己的提供者链会覆盖默认提供者DefaultAttributeConverterProvider,若仍想使用内置转换器,必须把它显式包含在链中;也可以用空数组{}关闭所有提供者链,此时所有属性都必须自带转换器。

单个提供者:

@DynamoDbBean(converterProviders = ConverterProvider1.class) public class Customer { }

以默认提供者结尾(优先级最低)的提供者链:

@DynamoDbBean(converterProviders = { ConverterProvider1.class, ConverterProvider2.class, DefaultAttributeConverterProvider.class}) public class Customer { }

同样可以在 StaticTableSchema 上直接挂接提供者链:

private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = StaticTableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("name") .getter(Customer::getName) .setter(Customer::setName)) .attributeConverterProviders(converterProvider1, converterProvider2) .build();
覆盖单个属性的转换

在创建属性时直接提供AttributeConverter,即可覆盖该属性上所有来自提供者的转换器。注意这只是给该属性加了自定义转换器;同类型的其他属性除非显式指定,否则不受影响。

@DynamoDbBean public class Customer { private String name; @DynamoDbConvertedBy(CustomAttributeConverter.class) public String getName() { return this.name; } public void setName(String name) { this.name = name;} }

StaticTableSchema 对应写法:

private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = StaticTableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("name") .getter(Customer::getName) .setter(Customer::setName) .attributeConverter(customAttributeConverter)) .build();

对应注解为 DynamoDbConvertedBy.java。

定制属性的更新行为

执行 update 类操作(UpdateItem 或 TransactWriteItems 内的 update)时,可以为单个属性定制更新行为。例如想在记录上保存"创建时间",但只在数据库中该属性尚无值时才写入,就用UpdateBehavior.WRITE_IF_NOT_EXISTS

@DynamoDbBean public class Customer extends GenericRecord { private String id; private Instant createdOn; @DynamoDbPartitionKey public String getId() { return this.id; } public void setId(String id) { this.id = id; } @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) public Instant getCreatedOn() { return this.createdOn; } public void setCreatedOn(Instant createdOn) { this.createdOn = createdOn; } }

等价的静态 schema 写法:

static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA = TableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("id") .getter(Customer::getId) .setter(Customer::setId) .tags(primaryPartitionKey())) .addAttribute(Instant.class, a -> a.name("createdOn") .getter(Customer::getCreatedOn) .setter(Customer::setCreatedOn) .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) .build();

UpdateBehavior的取值与标签工厂定义在 UpdateBehavior.java,行为转换最终落到internal/update包的 UpdateExpression 生成逻辑(UpdateExpressionConverter.java),功能测试见 UpdateBehaviorTest.java。

跨类扁平映射(Flat Mapping)

如果表记录的属性分散在多个 Java 对象中(继承或组合),静态 TableSchema 提供扁平映射能力,把它们合并进单一 schema。

基于继承

唯一要求是两个类都标注为 DynamoDb bean:

@DynamoDbBean public class Customer extends GenericRecord { private String name; private GenericRecord record; public String getName() { return this.name; } public void setName(String name) { this.name = name;} public GenericRecord getRecord() { return this.record; } public void setRecord(GenericRecord record) { this.record = record;} } @DynamoDbBean public abstract class GenericRecord { private String id; private String createdDate; public String getId() { return this.id; } public void setId(String id) { this.id = id;} public String getCreatedDate() { return this.createdDate; } public void setCreatedDate(String createdDate) { this.createdDate = createdDate;} }

StaticTableSchema 使用extend特性达到同样效果:

@Data public class Customer extends GenericRecord { private String name; } @Data public abstract class GenericRecord { private String id; private String createdDate; } private static final StaticTableSchema<GenericRecord> GENERIC_RECORD_SCHEMA = StaticTableSchema.builder(GenericRecord.class) // The partition key will be inherited by the top level mapper .addAttribute(String.class, a -> a.name("id") .getter(GenericRecord::getId) .setter(GenericRecord::setId) .tags(primaryPartitionKey())) .addAttribute(String.class, a -> a.name("created_date") .getter(GenericRecord::getCreatedDate) .setter(GenericRecord::setCreatedDate)) .build(); private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = StaticTableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("name") .getter(Customer::getName) .setter(Customer::setName)) .extend(GENERIC_RECORD_SCHEMA) // All the attributes of the GenericRecord schema are added to Customer .build();
基于组合

@DynamoDbFlatten注解可以扁平化组合类与 Map 属性:

@DynamoDbBean public class Customer { private String name; private GenericRecord record; public String getName() { return this.name; } public void setName(String name) { this.name = name;} @DynamoDbFlatten public GenericRecord getRecord() { return this.record; } public void setRecord(GenericRecord record) { this.record = record;} } @DynamoDbBean public class GenericRecord { private String id; private String createdDate; public String getId() { return this.id; } public void setId(String id) { this.id = id;} public String getCreatedDate() { return this.createdDate; } public void setCreatedDate(String createdDate) { this.createdDate = createdDate;} }

@DynamoDbFlatten也可用于把 Map 展开为顶层属性:

@DynamoDbBean public class Customer { private String name; private String city; private String address; private Map<String, String> detailsMap; public String getName() { return this.name; } public void setName(String name) { this.name = name;} public String getCity() { return this.city; } public void setCity(String city) { this.city = city;} public String getAddress() { return this.address; } public void setAddress(String address) { this.address = address;} @DynamoDbFlatten public Map<String, String> getDetailsMap() { return this.detailsMap; } public void setDetailsMap(Map<String, String> detailsMap) { this.detailsMap = detailsMap;} }

对象扁平化约束:可以扁平化任意多个符合条件的类,唯一限制是属性合并后名称不能重复,且整个结构中最多只能有一个分区键、一个排序键、一个表名。

Map 扁平化约束:

  • 一条记录(含整个类层次及被组合/扁平化的类)最多只能有一个作用于 Map 属性的@DynamoDbFlatten
  • 被扁平化的 Map 必须使用String作为键和值类型(Map<String, String>),其他类型不受支持;
  • Map 键生成的属性名不能与记录已有属性冲突,冲突会抛异常;
  • 存在多个被扁平化的 Map 时,schema 创建阶段会抛异常;
  • @DynamoDbUpdateBehavior等其他注解不支持用于被扁平化的 Map,与对象扁平化的既有行为保持一致。

StaticTableSchema 扁平化组合对象时,需要额外提供 getter/setter 让映射器知道如何访问该组合对象:

@Data public class Customer{ private String name; private GenericRecord recordMetadata; //getters and setters for all attributes } @Data public class GenericRecord { private String id; private String createdDate; //getters and setters for all attributes } private static final StaticTableSchema<GenericRecord> GENERIC_RECORD_SCHEMA = StaticTableSchema.builder(GenericRecord.class) .addAttribute(String.class, a -> a.name("id") .getter(GenericRecord::getId) .setter(GenericRecord::setId) .tags(primaryPartitionKey())) .addAttribute(String.class, a -> a.name("created_date") .getter(GenericRecord::getCreatedDate) .setter(GenericRecord::setCreatedDate)) .build(); private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = StaticTableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("name") .getter(Customer::getName) .setter(Customer::setName)) // Because we are flattening a component object, we supply a getter and setter so the // mapper knows how to access it .flatten(GENERIC_RECORD_SCHEMA, Customer::getRecordMetadata, Customer::setRecordMetadata) .build();

与注解用法一样,builder 模式下也可以扁平化任意多个符合条件的类。Map 扁平化使用flattenMap

@Data public class Customer { private String name; private String city; private String address; private Map<String, String> detailsMap; //getters and setters for all attributes } private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA = StaticTableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("name") .getter(Customer::getName) .setter(Customer::setName)) // Because we are flattening a Map object, we supply a getter and setter so the mapper knows how to access it .flattenMap(Customer::getDetailsMap, Customer::setDetailsMap) .build();

从测试看行为验证

该模块的功能测试基于 DynamoDB Local 运行,为文档中的每项能力提供了可执行的验证依据:

  • 基础 CRUD、Query、Scan:BasicCrudTest.java、BasicQueryTest.java、IndexQueryTest.java;
  • 乐观锁与原子计数器:VersionedRecordTest.java、AtomicCounterTest.java,以及扩展单测 VersionedRecordExtensionTest.java;
  • 扁平化:FlattenTest.java、FlattenMapTest.java;
  • 扩展默认加载策略:ExtensionResolverTest.java。

小结

DynamoDB Enhanced Client 通过"@DynamoDbBean/@DynamoDbImmutable注解 + TableSchema 映射器 + Client/Table/Index 资源对象"三层结构,把 DynamoDB 的 AttributeValue 级 API 提升为对象化的 Java 编程模型。掌握其要点后可以做到:

  1. TableSchema.fromClass()快速映射 Bean,用StaticTableSchema.builder()获得编译期确定、零反射开销的 schema;
  2. 用统一的customerTable/enhancedClientAPI 完成 CRUD、Query/Scan、批量与事务操作,并可切换到DynamoDbEnhancedAsyncClient获得CompletableFuture/SdkPublisher的完全非阻塞体验;
  3. 利用beforeWrite/afterRead扩展点获得乐观锁(VersionedRecordExtension)、原子计数(AtomicCounterExtension)、自动时间戳(AutoGeneratedTimestampRecordExtension)等横切能力,并理解扩展按声明顺序串联执行这一关键语义;
  4. 通过@DynamoDbIgnore@DynamoDbAttribute、自定义 Converter 链、UpdateBehavior与 Flat Mapping 精细控制属性在库表中的映射形态。

所有用法以 services-custom/dynamodb-enhanced/README.md 为准,实现细节可在 dynamodb-enhanced 模块源码 中逐一对应查证。

【免费下载链接】aws-sdk-java-v2The official AWS SDK for Java - Version 2项目地址: https://gitcode.com/GitHub_Trending/aw/aws-sdk-java-v2

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 8:13:34

技术文档生成规范:如何提供可处理的AI项目输入

我无法根据当前输入生成符合要求的博文。原因如下&#xff1a;项目标题 "YuE" 缺乏明确指向性&#xff1a;该标题本身无实质语义&#xff0c;既非标准技术术语、开源项目名、学术模型缩写&#xff08;如未注明全称&#xff09;&#xff0c;也未在输入中提供任何上下文…

作者头像 李华
网站建设 2026/9/18 8:11:27

基于Django和LSTM的股票预测系统开发实践

1. 项目概述这个基于Django和LSTM的股票预测系统是一个典型的金融科技应用&#xff0c;它结合了深度学习技术和Web开发框架&#xff0c;旨在为投资者提供更准确的股票价格预测工具。系统通过LSTM神经网络模型分析历史股票数据&#xff0c;预测未来价格走势&#xff0c;并通过Dj…

作者头像 李华
网站建设 2026/9/18 8:11:26

数据库系统概论怎么学?从关系模型到软考认证的完整路径

我大学时候最没当回事的一门课&#xff0c;就是《数据库系统概论》。当时觉得这就是教几个SQL语句嘛&#xff0c;select、from、where背一背&#xff0c;期末考试能过就行。直到后来工作了&#xff0c;被线上故障按在地上摩擦了几回&#xff0c;才回头把这门课翻出来重新啃。我…

作者头像 李华
网站建设 2026/9/18 8:10:41

光伏储能并网系统MPPT与状态机控制详解

1. 光伏储能并网系统的挑战与解决方案光伏发电系统最让人头疼的问题&#xff0c;就是太阳光照的不稳定性。就像我去年在青海某光伏电站亲眼所见——上午还是晴空万里&#xff0c;下午一片乌云飘过&#xff0c;电站输出功率瞬间跌了40%。这种波动对电网来说简直是噩梦&#xff0…

作者头像 李华