Skip to content

Commit

Permalink
Add support for flattening prefixes to DynamoDB fields
Browse files Browse the repository at this point in the history
This makes enables clients to avoid conflicts if flattened schemas happen
to share a field name. The design maintains backwards compatibility with
existing codebases by requiring that users opt-in to this behavior
explicitly. While this increases the mental overhead there is a design
to enable the auto-prefixing as the default behavior when creating the
mapper. However, that is outside of the scope of this current implementation.
  • Loading branch information
Andy Kiesler committed Sep 16, 2024
1 parent 317d138 commit 0aec8f1
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AmazonDynamoDB-3f6308a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "Amazon DynamoDB",
"contributor": "kiesler",
"type": "feature",
"description": "Support optional prefix for `@DynamoDbFlatten` fields"
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
if (dynamoDbFlatten != null) {
builder.flatten(TableSchema.fromClass(propertyDescriptor.getReadMethod().getReturnType()),
getterForProperty(propertyDescriptor, beanClass),
setterForProperty(propertyDescriptor, beanClass));
setterForProperty(propertyDescriptor, beanClass),
getFlattenedPrefix(propertyDescriptor, dynamoDbFlatten));
} else {
AttributeConfiguration attributeConfiguration =
resolveAttributeConfiguration(propertyDescriptor);
Expand All @@ -225,6 +226,14 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
return builder.build();
}

private static String getFlattenedPrefix(PropertyDescriptor propertyDescriptor, DynamoDbFlatten dynamoDbFlatten) {
boolean useAutoPrefix = DynamoDbFlatten.AUTO_PREFIX.equals(dynamoDbFlatten.prefix());
if (!useAutoPrefix) {
return dynamoDbFlatten.prefix();
}
return attributeNameForProperty(propertyDescriptor) + ".";
}

private static AttributeConfiguration resolveAttributeConfiguration(PropertyDescriptor propertyDescriptor) {
boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor,
DynamoDbPreserveEmptyObject.class) != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ private static <T, B> StaticImmutableTableSchema<T, B> createStaticImmutableTab
if (dynamoDbFlatten != null) {
builder.flatten(TableSchema.fromClass(propertyDescriptor.getter().getReturnType()),
getterForProperty(propertyDescriptor, immutableClass),
setterForProperty(propertyDescriptor, builderClass));
setterForProperty(propertyDescriptor, builderClass),
getFlattenedPrefix(propertyDescriptor, dynamoDbFlatten));
} else {
AttributeConfiguration beanAttributeConfiguration = resolveAttributeConfiguration(propertyDescriptor);
ImmutableAttribute.Builder<T, B, ?> attributeBuilder =
Expand All @@ -225,6 +226,14 @@ private static <T, B> StaticImmutableTableSchema<T, B> createStaticImmutableTab
return builder.build();
}

private static String getFlattenedPrefix(ImmutablePropertyDescriptor propertyDescriptor, DynamoDbFlatten dynamoDbFlatten) {
boolean useAutoPrefix = DynamoDbFlatten.AUTO_PREFIX.equals(dynamoDbFlatten.prefix());
if (!useAutoPrefix) {
return dynamoDbFlatten.prefix();
}
return attributeNameForProperty(propertyDescriptor) + ".";
}

private static List<AttributeConverterProvider> createConverterProvidersFromAnnotation(Class<?> immutableClass,
DynamoDbImmutable dynamoDbImmutable) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,62 @@
* This annotation is used to flatten all the attributes of a separate DynamoDb bean that is stored in the current bean
* object and add them as top level attributes to the record that is read and written to the database. The target bean
* to flatten must be specified as part of this annotation.
* The flattening behavior can be controlled by the prefix value of the annotation.
* The default behavior is that no prefix is applied (this is done for backwards compatability).
* If a String value is supplied then that is prefixed to the attribute names.
* If a value of {@code DynamoDbFlatten.AUTO_PREFIX} is supplied then the attribute name of the flattened bean appended
* with a period ('.') is used as the prefix.
*
* Example, given the following classes:
* <pre>{@code
* @DynamoDbBean
* public class Flattened {
* String getValue();
* }
*
* @DynamoDbBean
* public class Record {
* @DynamoDbFlatten
* Flattened getNoPrefix(); // translates to attribute 'value'
* @DynamoDbFlatten(prefix = "prefix-")
* Flattened getExplicitPrefix(); // translates to attribute 'prefix-value'
* @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
* Flattened getInferredPrefix(); // translates to attribute 'inferredPrefix.value'
* @DynamoDbAttribute("custom")
* @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
* Flattened getFlattened(); // translates to attribute 'custom.value'
* }
*}</pre>
* They would be mapped as such:
* <pre>{@code
* {
* "value": {"S": "..."},
* "prefix-value": {"S": "..."},
* "inferredPrefix.value": {"S": "..."},
* "custom.value": {"S": "..."},
* }
* }</pre>
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@SdkPublicApi
public @interface DynamoDbFlatten {
/**
* Values used to denote that the mapper should append the current attribute name to flattened fields.
*/
String AUTO_PREFIX = "AUTO_PREFIX";

/**
* @deprecated This is no longer used, the class type of the attribute will be used instead.
*/
@Deprecated
Class<?> dynamoDbBeanClass() default Object.class;

/**
* Optional prefix to append to the flattened bean attributes in the schema.
* Specifying a value of {@code DynamoDbFlatten.AUTO_PREFIX} will use the annotated methods attribute name as the
* prefix.
* default: {@code ""} (No prefix)
*/
String prefix() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
Expand All @@ -54,6 +55,7 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EnumBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ExtendedBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean;
Expand Down Expand Up @@ -178,18 +180,57 @@ public void dynamoDbAttribute_remapsAttributeName() {
@Test
public void dynamoDbFlatten_correctlyFlattensBeanAttributes() {
BeanTableSchema<FlattenedBeanBean> beanTableSchema = BeanTableSchema.create(FlattenedBeanBean.class);

assertThat(beanTableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2",
"prefix-attribute2", "autoPrefixBean.attribute2", "custom.attribute2"));

AbstractBean abstractBean = new AbstractBean();
abstractBean.setAttribute2("two");
FlattenedBeanBean flattenedBeanBean = new FlattenedBeanBean();
flattenedBeanBean.setId("id-value");
flattenedBeanBean.setAttribute1("one");
flattenedBeanBean.setAbstractBean(abstractBean);
flattenedBeanBean.setExplicitPrefixBean(abstractBean);
flattenedBeanBean.setAutoPrefixBean(abstractBean);
flattenedBeanBean.setCustomPrefixBean(abstractBean);

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedBeanBean, false);
assertThat(itemMap.size(), is(3));
assertThat(itemMap.size(), is(6));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("autoPrefixBean.attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
}

@Test
public void dynamoDbFlatten_correctlyGetFlattenedBeanAttributes() {
BeanTableSchema<FlattenedBeanBean> tableSchema = BeanTableSchema.create(FlattenedBeanBean.class);

AbstractBean abstractBean = new AbstractBean();
abstractBean.setAttribute2("two");
AbstractBean explicitPrefixBean = new AbstractBean();
explicitPrefixBean.setAttribute2("three");
AbstractBean autoPrefixBean = new AbstractBean();
autoPrefixBean.setAttribute2("four");
AbstractBean customPrefixBean = new AbstractBean();
customPrefixBean.setAttribute2("five");

FlattenedBeanBean bean = new FlattenedBeanBean();
bean.setId("id-value");
bean.setAttribute1("one");
bean.setAbstractBean(abstractBean);
bean.setExplicitPrefixBean(explicitPrefixBean);
bean.setAutoPrefixBean(autoPrefixBean);
bean.setCustomPrefixBean(customPrefixBean);

assertThat(tableSchema.attributeValue(bean, "id"), equalTo(stringValue("id-value")));
assertThat(tableSchema.attributeValue(bean, "attribute1"), equalTo(stringValue("one")));
assertThat(tableSchema.attributeValue(bean, "attribute2"), equalTo(stringValue("two")));
assertThat(tableSchema.attributeValue(bean, "prefix-attribute2"), equalTo(stringValue("three")));
assertThat(tableSchema.attributeValue(bean, "autoPrefixBean.attribute2"), equalTo(stringValue("four")));
assertThat(tableSchema.attributeValue(bean, "custom.attribute2"), equalTo(stringValue("five")));
}

@Test
Expand Down Expand Up @@ -248,12 +289,18 @@ public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() {
flattenedImmutableBean.setId("id-value");
flattenedImmutableBean.setAttribute1("one");
flattenedImmutableBean.setAbstractImmutable(abstractImmutable);
flattenedImmutableBean.setExplicitPrefixImmutable(abstractImmutable);
flattenedImmutableBean.setAutoPrefixImmutable(abstractImmutable);
flattenedImmutableBean.setCustomPrefixImmutable(abstractImmutable);

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false);
assertThat(itemMap.size(), is(3));
assertThat(itemMap.size(), is(6));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("autoPrefixImmutable.attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

import static java.util.Collections.singletonMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue;
Expand All @@ -25,6 +28,8 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable;
Expand Down Expand Up @@ -213,37 +218,86 @@ public void documentImmutable_map_correctlyMapsImmutableAttributes() {
public void dynamoDbFlatten_correctlyFlattensBeanAttributes() {
ImmutableTableSchema<FlattenedBeanImmutable> tableSchema =
ImmutableTableSchema.create(FlattenedBeanImmutable.class);

assertThat(tableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2",
"prefix-attribute2", "autoPrefixBean.attribute2", "custom.attribute2"));

AbstractBean abstractBean = new AbstractBean();
abstractBean.setAttribute2("two");
FlattenedBeanImmutable flattenedBeanImmutable =
new FlattenedBeanImmutable.Builder().setId("id-value")
.setAttribute1("one")
.setAbstractBean(abstractBean)
.setExplicitPrefixBean(abstractBean)
.setAutoPrefixBean(abstractBean)
.setCustomPrefixBean(abstractBean)
.build();

Map<String, AttributeValue> itemMap = tableSchema.itemToMap(flattenedBeanImmutable, false);
assertThat(itemMap.size(), is(3));
assertThat(itemMap.size(), is(6));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("autoPrefixBean.attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
}

@Test
public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() {
ImmutableTableSchema<FlattenedImmutableImmutable> tableSchema =
ImmutableTableSchema.create(FlattenedImmutableImmutable.class);

assertThat(tableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2",
"prefix-attribute2", "autoPrefixImmutable.attribute2", "custom.attribute2"));

AbstractImmutable abstractImmutable = AbstractImmutable.builder().attribute2("two").build();
FlattenedImmutableImmutable FlattenedImmutableImmutable =
new FlattenedImmutableImmutable.Builder().setId("id-value")
.setAttribute1("one")
.setAbstractImmutable(abstractImmutable)
.setExplicitPrefixImmutable(abstractImmutable)
.setAutoPrefixImmutable(abstractImmutable)
.setCustomPrefixImmutable(abstractImmutable)
.build();

Map<String, AttributeValue> itemMap = tableSchema.itemToMap(FlattenedImmutableImmutable, false);
assertThat(itemMap.size(), is(3));
assertThat(itemMap.size(), is(6));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("autoPrefixImmutable.attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
}

@Test
public void dynamoDbFlatten_correctlyGetFlattenedBeanAttributes() {
ImmutableTableSchema<FlattenedBeanImmutable> tableSchema =
ImmutableTableSchema.create(FlattenedBeanImmutable.class);

AbstractBean abstractBean = new AbstractBean();
abstractBean.setAttribute2("two");
AbstractBean explicitPrefixBean = new AbstractBean();
explicitPrefixBean.setAttribute2("three");
AbstractBean autoPrefixBean = new AbstractBean();
autoPrefixBean.setAttribute2("four");
AbstractBean customPrefixBean = new AbstractBean();
customPrefixBean.setAttribute2("five");
FlattenedBeanImmutable bean = new FlattenedBeanImmutable.Builder().setId("id-value")
.setAttribute1("one")
.setAbstractBean(abstractBean)
.setExplicitPrefixBean(explicitPrefixBean)
.setAutoPrefixBean(autoPrefixBean)
.setCustomPrefixBean(customPrefixBean)
.build();

assertThat(tableSchema.attributeValue(bean, "id"), equalTo(stringValue("id-value")));
assertThat(tableSchema.attributeValue(bean, "attribute1"), equalTo(stringValue("one")));
assertThat(tableSchema.attributeValue(bean, "attribute2"), equalTo(stringValue("two")));
assertThat(tableSchema.attributeValue(bean, "prefix-attribute2"), equalTo(stringValue("three")));
assertThat(tableSchema.attributeValue(bean, "autoPrefixBean.attribute2"), equalTo(stringValue("four")));
assertThat(tableSchema.attributeValue(bean, "custom.attribute2"), equalTo(stringValue("five")));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans;

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.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
Expand All @@ -24,6 +25,9 @@ public class FlattenedBeanBean {
private String id;
private String attribute1;
private AbstractBean abstractBean;
private AbstractBean explicitPrefixBean;
private AbstractBean autoPrefixBean;
private AbstractBean customPrefixBean;

@DynamoDbPartitionKey
public String getId() {
Expand All @@ -47,4 +51,29 @@ public AbstractBean getAbstractBean() {
public void setAbstractBean(AbstractBean abstractBean) {
this.abstractBean = abstractBean;
}

@DynamoDbFlatten(prefix = "prefix-")
public AbstractBean getExplicitPrefixBean() {
return explicitPrefixBean;
}
public void setExplicitPrefixBean(AbstractBean explicitPrefixBean) {
this.explicitPrefixBean = explicitPrefixBean;
}

@DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
public AbstractBean getAutoPrefixBean() {
return autoPrefixBean;
}
public void setAutoPrefixBean(AbstractBean autoPrefixBean) {
this.autoPrefixBean = autoPrefixBean;
}

@DynamoDbAttribute("custom")
@DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
public AbstractBean getCustomPrefixBean() {
return customPrefixBean;
}
public void setCustomPrefixBean(AbstractBean customPrefixBean) {
this.customPrefixBean = customPrefixBean;
}
}
Loading

0 comments on commit 0aec8f1

Please sign in to comment.