Skip to content
56 changes: 56 additions & 0 deletions services-custom/dynamodb-enhanced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,59 @@ private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
```
Just as for annotations, you can flatten as many different eligible classes as you like using the
builder pattern.



## Polymorphic Types Support

The Enhanced Client now supports **polymorphic type hierarchies**, allowing multiple subclasses to be stored in the same table.

### Usage Example: Person Hierarchy

```java
@DynamoDbBean
@DynamoDbSupertype(
value = {
@DynamoDbSupertype.Subtype(discriminatorValue = "EMPLOYEE", subtypeClass = Employee.class),
@DynamoDbSupertype.Subtype(discriminatorValue = "CUSTOMER", subtypeClass = Customer.class)
},
discriminatorAttributeName = "discriminatorType" // optional, defaults to "type"
)
public class Person {}

@DynamoDbBean
public class Employee extends Person {
private String employeeId;
public String getEmployeeId() { return employeeId; }
public void setEmployeeId(String id) { this.employeeId = id; }
}

@DynamoDbBean
public class Customer extends Person {
private String customerId;
public String getCustomerId() { return customerId; }
public void setCustomerId(String id) { this.customerId = id; }
}
```

**Notes:**
- By default, the discriminator attribute is `"type"` unless overridden.

### Static/Immutable Schema Support

Polymorphism works for both **bean-style** and **immutable/builder-based** classes.

```java
// Obtain schema for Person hierarchy
TableSchema<Person> schema = TableSchema.fromClass(Person.class);

// Serialize Employee → DynamoDB item
Employee e = new Employee();
e.setEmployeeId("E123");
Map<String, AttributeValue> item = schema.itemToMap(e, false);
// → {"employeeId":"E123", "discriminatorType":"EMPLOYEE"}

// Deserialize back
Person restored = schema.mapToItem(item);
// → returns Employee instance
```
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.TableSchemaFactory;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

Expand Down Expand Up @@ -200,16 +201,7 @@ static <T> ImmutableTableSchema<T> fromImmutableClass(ImmutableTableSchemaParams
* @return An initialized {@link TableSchema}
*/
static <T> TableSchema<T> fromClass(Class<T> annotatedClass) {
if (annotatedClass.getAnnotation(DynamoDbImmutable.class) != null) {
return fromImmutableClass(annotatedClass);
}

if (annotatedClass.getAnnotation(DynamoDbBean.class) != null) {
return fromBean(annotatedClass);
}

throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " +
"\"" + annotatedClass + "\"]");
return TableSchemaFactory.fromClass(annotatedClass);
}

/**
Expand Down Expand Up @@ -344,4 +336,30 @@ default T mapToItem(Map<String, AttributeValue> attributeMap, boolean preserveEm
default AttributeConverter<T> converterForAttribute(Object key) {
throw new UnsupportedOperationException();
}

/**
* If applicable, returns a {@link TableSchema} for a specific object subtype. If the implementation does not support
* polymorphic mapping, then this method will, by default, return the current instance. This method is primarily used to pass
* the right contextual information to extensions when they are invoked mid-operation. This method is not required to get a
* polymorphic {@link TableSchema} to correctly map subtype objects using 'mapToItem' or 'itemToMap'.
*
* @param itemContext the subtype object to retrieve the subtype {@link TableSchema} for.
* @return the subtype {@link TableSchema} or the current {@link TableSchema} if subtypes are not supported.
*/
default TableSchema<? extends T> subtypeTableSchema(T itemContext) {
return this;
}

/**
* If applicable, returns a {@link TableSchema} for a specific object subtype. If the implementation does not support
* polymorphic mapping, then this method will, by default, return the current instance. This method is primarily used to pass
* the right contextual information to extensions when they are invoked mid-operation. This method is not required to get a
* polymorphic {@link TableSchema} to correctly map subtype objects using 'mapToItem' or 'itemToMap'.
*
* @param itemContext the subtype object map to retrieve the subtype {@link TableSchema} for.
* @return the subtype {@link TableSchema} or the current {@link TableSchema} if subtypes are not supported.
*/
default TableSchema<? extends T> subtypeTableSchema(Map<String, AttributeValue> itemContext) {
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,14 @@ public static <T> T readAndTransformSingleItem(Map<String, AttributeValue> itemM
}

if (dynamoDbEnhancedClientExtension != null) {
TableSchema<? extends T> subtypeTableSchema = tableSchema.subtypeTableSchema(itemMap);

ReadModification readModification = dynamoDbEnhancedClientExtension.afterRead(
DefaultDynamoDbExtensionContext.builder()
.items(itemMap)
.tableSchema(tableSchema)
.tableSchema(subtypeTableSchema)
.operationContext(operationContext)
.tableMetadata(tableSchema.tableMetadata())
.tableMetadata(subtypeTableSchema.tableMetadata())
.build());
if (readModification != null && readModification.transformedItem() != null) {
return tableSchema.mapToItem(readModification.transformedItem());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,14 @@ public PutItemRequest generateRequest(TableSchema<T> tableSchema,
throw new IllegalArgumentException("PutItem cannot be executed against a secondary index.");
}

TableMetadata tableMetadata = tableSchema.tableMetadata();
T item = request.map(PutItemEnhancedRequest::item, TransactPutItemEnhancedRequest::item);
TableSchema<? extends T> subtypeTableSchema = tableSchema.subtypeTableSchema(item);
TableMetadata tableMetadata = subtypeTableSchema.tableMetadata();

// Fail fast if required primary partition key does not exist and avoid the call to DynamoDb
tableMetadata.primaryPartitionKey();

boolean alwaysIgnoreNulls = true;
T item = request.map(PutItemEnhancedRequest::item, TransactPutItemEnhancedRequest::item);
Map<String, AttributeValue> itemMap = tableSchema.itemToMap(item, alwaysIgnoreNulls);

WriteModification transformation =
Expand All @@ -95,7 +96,7 @@ public PutItemRequest generateRequest(TableSchema<T> tableSchema,
.items(itemMap)
.operationContext(operationContext)
.tableMetadata(tableMetadata)
.tableSchema(tableSchema)
.tableSchema(subtypeTableSchema)
.operationName(operationName())
.build())
: null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,17 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,

Map<String, AttributeValue> itemMap = ignoreNullsMode == IgnoreNullsMode.SCALAR_ONLY ?
transformItemToMapForUpdateExpression(itemMapImmutable) : itemMapImmutable;

TableMetadata tableMetadata = tableSchema.tableMetadata();

TableSchema<? extends T> subtypeTableSchema = tableSchema.subtypeTableSchema(item);
TableMetadata tableMetadata = subtypeTableSchema.tableMetadata();

WriteModification transformation =
extension != null
? extension.beforeWrite(DefaultDynamoDbExtensionContext.builder()
.items(itemMap)
.operationContext(operationContext)
.tableMetadata(tableMetadata)
.tableSchema(tableSchema)
.tableSchema(subtypeTableSchema)
.operationName(operationName())
.build())
: null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject;
import software.amazon.awssdk.utils.StringUtils;

Expand Down Expand Up @@ -100,7 +99,7 @@
* public Instant getCreatedDate() { return this.createdDate; }
* public void setCreatedDate(Instant createdDate) { this.createdDate = createdDate; }
* }
*
* </code>
* </pre>
*
* Creating an {@link BeanTableSchema} is a moderately expensive operation, and should be performed sparingly. This is
Expand Down Expand Up @@ -167,39 +166,21 @@ public static <T> BeanTableSchema<T> create(BeanTableSchemaParams<T> params) {
new MetaTableSchemaCache()));
}

private static <T> BeanTableSchema<T> create(BeanTableSchemaParams<T> params, MetaTableSchemaCache metaTableSchemaCache) {
static <T> BeanTableSchema<T> create(BeanTableSchemaParams<T> params, MetaTableSchemaCache metaTableSchemaCache) {
Class<T> beanClass = params.beanClass();
debugLog(beanClass, () -> "Creating bean schema");
// Fetch or create a new reference to this yet-to-be-created TableSchema in the cache
MetaTableSchema<T> metaTableSchema = metaTableSchemaCache.getOrCreate(beanClass);

BeanTableSchema<T> newTableSchema =
new BeanTableSchema<>(createStaticTableSchema(params.beanClass(), params.lookup(), metaTableSchemaCache));
BeanTableSchema<T> newTableSchema = createWithoutUsingCache(beanClass, params.lookup(), metaTableSchemaCache);
metaTableSchema.initialize(newTableSchema);
return newTableSchema;
}

// Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite
// recursion
static <T> TableSchema<T> recursiveCreate(Class<T> beanClass, MethodHandles.Lookup lookup,
MetaTableSchemaCache metaTableSchemaCache) {
Optional<MetaTableSchema<T>> metaTableSchema = metaTableSchemaCache.get(beanClass);

// If we get a cache hit...
if (metaTableSchema.isPresent()) {
// Either: use the cached concrete TableSchema if we have one
if (metaTableSchema.get().isInitialized()) {
return metaTableSchema.get().concreteTableSchema();
}

// Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
// initialized later as the chain completes
return metaTableSchema.get();
}

// Otherwise: cache doesn't know about this class; create a new one from scratch
return create(BeanTableSchemaParams.builder(beanClass).lookup(lookup).build());

static <T> BeanTableSchema<T> createWithoutUsingCache(Class<T> beanClass,
MethodHandles.Lookup lookup,
MetaTableSchemaCache metaTableSchemaCache) {
return new BeanTableSchema<>(createStaticTableSchema(beanClass, lookup, metaTableSchemaCache));
}

private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanClass,
Expand Down Expand Up @@ -363,22 +344,15 @@ private static EnhancedType<?> convertTypeToEnhancedType(Type type,
clazz = (Class<?>) type;
}

if (clazz != null) {
if (clazz != null && TableSchemaFactory.isDynamoDbAnnotatedClass(clazz)) {
Consumer<EnhancedTypeDocumentConfiguration.Builder> attrConfiguration =
b -> b.preserveEmptyObject(attributeConfiguration.preserveEmptyObject())
.ignoreNulls(attributeConfiguration.ignoreNulls());

if (clazz.getAnnotation(DynamoDbImmutable.class) != null) {
return EnhancedType.documentOf(
(Class<Object>) clazz,
(TableSchema<Object>) ImmutableTableSchema.recursiveCreate(clazz, lookup, metaTableSchemaCache),
attrConfiguration);
} else if (clazz.getAnnotation(DynamoDbBean.class) != null) {
return EnhancedType.documentOf(
(Class<Object>) clazz,
(TableSchema<Object>) BeanTableSchema.recursiveCreate(clazz, lookup, metaTableSchemaCache),
attrConfiguration);
}
return EnhancedType.documentOf(
(Class<Object>) clazz,
(TableSchema<Object>) TableSchemaFactory.fromClass(clazz, lookup, metaTableSchemaCache),
attrConfiguration);
}

return EnhancedType.of(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticGetterMethod;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls;
Expand Down Expand Up @@ -99,6 +98,7 @@
* public Customer build() { ... };
* }
* }
* </code>
* </pre>
*
* Creating an {@link ImmutableTableSchema} is a moderately expensive operation, and should be performed sparingly. This is
Expand Down Expand Up @@ -161,42 +161,24 @@ public static <T> ImmutableTableSchema<T> create(Class<T> immutableClass) {
return create(ImmutableTableSchemaParams.builder(immutableClass).build());
}

private static <T> ImmutableTableSchema<T> create(ImmutableTableSchemaParams<T> params,
MetaTableSchemaCache metaTableSchemaCache) {
static <T> ImmutableTableSchema<T> create(ImmutableTableSchemaParams<T> params,
MetaTableSchemaCache metaTableSchemaCache) {
debugLog(params.immutableClass(), () -> "Creating immutable schema");

// Fetch or create a new reference to this yet-to-be-created TableSchema in the cache
MetaTableSchema<T> metaTableSchema = metaTableSchemaCache.getOrCreate(params.immutableClass());

ImmutableTableSchema<T> newTableSchema =
new ImmutableTableSchema<>(createStaticImmutableTableSchema(params.immutableClass(),
params.lookup(),
metaTableSchemaCache));
ImmutableTableSchema<T> newTableSchema = createWithoutUsingCache(params.immutableClass(),
params.lookup(),
metaTableSchemaCache);
metaTableSchema.initialize(newTableSchema);
return newTableSchema;
}

// Called when creating an immutable TableSchema recursively. Utilizes the MetaTableSchema cache to stop infinite
// recursion
static <T> TableSchema<T> recursiveCreate(Class<T> immutableClass, MethodHandles.Lookup lookup,
MetaTableSchemaCache metaTableSchemaCache) {
Optional<MetaTableSchema<T>> metaTableSchema = metaTableSchemaCache.get(immutableClass);

// If we get a cache hit...
if (metaTableSchema.isPresent()) {
// Either: use the cached concrete TableSchema if we have one
if (metaTableSchema.get().isInitialized()) {
return metaTableSchema.get().concreteTableSchema();
}

// Or: return the uninitialized MetaTableSchema as this must be a recursive reference and it will be
// initialized later as the chain completes
return metaTableSchema.get();
}

// Otherwise: cache doesn't know about this class; create a new one from scratch
return create(ImmutableTableSchemaParams.builder(immutableClass).lookup(lookup).build(), metaTableSchemaCache);

static <T> ImmutableTableSchema<T> createWithoutUsingCache(Class<T> immutableClass,
MethodHandles.Lookup lookup,
MetaTableSchemaCache metaTableSchemaCache) {
return new ImmutableTableSchema<>(createStaticImmutableTableSchema(immutableClass, lookup, metaTableSchemaCache));
}

private static <T> StaticImmutableTableSchema<T, ?> createStaticImmutableTableSchema(
Expand Down Expand Up @@ -326,25 +308,15 @@ private static EnhancedType<?> convertTypeToEnhancedType(Type type,
clazz = (Class<?>) type;
}

if (clazz != null) {
if (clazz != null && TableSchemaFactory.isDynamoDbAnnotatedClass(clazz)) {
Consumer<EnhancedTypeDocumentConfiguration.Builder> attrConfiguration =
b -> b.preserveEmptyObject(attributeConfiguration.preserveEmptyObject())
.ignoreNulls(attributeConfiguration.ignoreNulls());
if (clazz.getAnnotation(DynamoDbImmutable.class) != null) {
return EnhancedType.documentOf(
(Class<Object>) clazz,
(TableSchema<Object>) ImmutableTableSchema.recursiveCreate(clazz,
lookup,
metaTableSchemaCache),
attrConfiguration);
} else if (clazz.getAnnotation(DynamoDbBean.class) != null) {
return EnhancedType.documentOf(
(Class<Object>) clazz,
(TableSchema<Object>) BeanTableSchema.recursiveCreate(clazz,
lookup,
metaTableSchemaCache),
attrConfiguration);
}

return EnhancedType.documentOf(
(Class<Object>) clazz,
(TableSchema<Object>) TableSchemaFactory.fromClass(clazz, lookup, metaTableSchemaCache),
attrConfiguration);
}

return EnhancedType.of(type);
Expand Down
Loading