Support optional prefix for @DynamoDbFlatten fields
Describe the feature
In order to provide better clarity to field names in Flattened DynamoDdBeans we need to be able to prefix field names with additional context. I propose adding an optional String prefix to the DynamoDbFlatten annotation that would allow users to supply a prefix that would be appended to flatten field names in the parent object. Leaving the property unset would result in the current behavior of no prefixing in order to maintain backwards compatability.
Use Case
I want to flatten multiple objects of the same type but with different meanings and not have their names conflict.
// Record.java
import java.time.Instant;
import lombok.Data;
import lombok.Getter;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
@Data
@DynamoDbBean
public class Record {
@Getter(onMethod_ = {@DynamoDbPartitionKey})
String id;
@Getter(onMethod_ = {@DynamoDbFlatten})
Edit created;
@Getter(onMethod_ = {@DynamoDbFlatten})
Edit updated;
@Data
@DynamoDbBean
public static class Edit {
String id;
Instant timestamp;
}
}
Proposed Solution
Extend the existing FlattenedMapper class to include an optional prefix that would be appended to each attribute when building the map of mappers.
public @interface DynamoDbFlatten {
String prefix default "";
}
DynamoDbFlatten dynamoDbFlatten = getPropertyAnnotation(propertyDescriptor, DynamoDbFlatten.class);
if (dynamoDbFlatten != null) {
builder.flatten(TableSchema.fromClass(propertyDescriptor.getter().getReturnType()),
getterForProperty(propertyDescriptor, immutableClass),
setterForProperty(propertyDescriptor, builderClass),
dynamoDbFlatten.prefix());
} else { ... }
flattenedMapper.otherItemTableSchema.attributeNames().forEach(
attrName -> {
final String attributeName = flattenedMapper.prefix + attrName
if (mutableAttributeNames.contains(attributeName)) {
throw new IllegalArgumentException(
"Attempt to add an attribute to a mapper that already has one with the same name. " +
"[Attribute name: " + attributeName + "]");
}
mutableAttributeNames.add(attributeName);
mutableFlattenedMappers.put(attributeName, flattenedMapper);
}
);
Other Information
Here is an example test written in Spock:
// Record.java
import java.time.Instant;
import lombok.Data;
import lombok.Getter;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
@Data
@DynamoDbBean
public class Record {
@Getter(onMethod_ = {@DynamoDbPartitionKey})
String id;
@Getter(onMethod_ = {@DynamoDbFlatten})
Edit created;
@Getter(onMethod_ = {@DynamoDbFlatten})
Edit updated;
@Data
@DynamoDbBean
public static class Edit {
String id;
Instant timestamp;
}
}
// DynamoDBSpec.groovy
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable
import software.amazon.awssdk.enhanced.dynamodb.TableSchema
import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension
import software.amazon.awssdk.enhanced.dynamodb.extensions.VersionedRecordExtension
import software.amazon.awssdk.services.dynamodb.local.embedded.DynamoDBEmbeddedRule
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
import spock.lang.Shared
import spock.lang.Specification
import java.time.Instant
class DynamoDBSpec extends Specification {
@Shared
DynamoDbClient dynamoDbClient = DynamoDbEnhancedClient.create()
@Shared
DynamoDbEnhancedClient dynamoDb = DynamoDbEnhancedClient.builder()
.dynamoDbClient(dynamoDbClient())
.build()
@Shared
DynamoDbTable<Record> recordsTable = dynamoDb.table("Records", TableSchema.fromClass(Record.class))
void setup() {
recordsTable.createTable()
}
void cleanup() {
recordsTable.deleteTable()
}
def "flattened records should not conflict"() {
given:
def created = new Record.Edit().tap {
id = "Creating User"
timestamp = Instant.EPOCH
}
def updated = new Record.Edit().tap {
id = "Updating User"
timestamp = Instant.EPOCH.plusSeconds(10L)
}
def record = new Record().tap {
id = "Record ID"
it.created = created
it.updated = updated
}
when:
recordsTable.putItem(record)
then:
def item = recordsTable.getItem(record)
and:
item == record
}
}
// Stacktrace
Attempt to add an attribute to a mapper that already has one with the same name. [Attribute name: id]
java.lang.IllegalArgumentException: Attempt to add an attribute to a mapper that already has one with the same name. [Attribute name: id]
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.lambda$null$2(StaticImmutableTableSchema.java:187)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at java.base/java.util.Collections$UnmodifiableCollection.forEach(Collections.java:1085)
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.lambda$new$3(StaticImmutableTableSchema.java:184)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.<init>(StaticImmutableTableSchema.java:182)
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.<init>(StaticImmutableTableSchema.java:80)
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema$Builder.build(StaticImmutableTableSchema.java:429)
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema.<init>(StaticTableSchema.java:69)
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema.<init>(StaticTableSchema.java:67)
at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema$Builder.build(StaticTableSchema.java:259)
at software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema.createStaticTableSchema(BeanTableSchema.java:218)
at software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema.create(BeanTableSchema.java:138)
at software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema.create(BeanTableSchema.java:129)
at software.amazon.awssdk.enhanced.dynamodb.TableSchema.fromBean(TableSchema.java:83)
at software.amazon.awssdk.enhanced.dynamodb.TableSchema.fromClass(TableSchema.java:126)
at com.amazon.aet.comms.configuration.utils.DynamoDBSpec.setupSpec(DynamoDBSpec.groovy:29)
Acknowledgements
- [x] I may be able to implement this feature request
- [x] This feature might incur a breaking change
AWS Java SDK version used
2.19.17
JDK version used
openjdk version "1.8.0_352"
Operating System and version
macOS 12.6.1
Acknowledged. Added this to our backlog.
@akiesler thank you for reaching out.
+1 This would be a very useful feature for our use case as well.
@debora-ito any update?