Skip to content

Commit 0aec8f1

Browse files
author
Andy Kiesler
committed
Add support for flattening prefixes to DynamoDB fields
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.
1 parent 317d138 commit 0aec8f1

File tree

10 files changed

+318
-6
lines changed

10 files changed

+318
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "Amazon DynamoDB",
3+
"contributor": "kiesler",
4+
"type": "feature",
5+
"description": "Support optional prefix for `@DynamoDbFlatten` fields"
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
203203
if (dynamoDbFlatten != null) {
204204
builder.flatten(TableSchema.fromClass(propertyDescriptor.getReadMethod().getReturnType()),
205205
getterForProperty(propertyDescriptor, beanClass),
206-
setterForProperty(propertyDescriptor, beanClass));
206+
setterForProperty(propertyDescriptor, beanClass),
207+
getFlattenedPrefix(propertyDescriptor, dynamoDbFlatten));
207208
} else {
208209
AttributeConfiguration attributeConfiguration =
209210
resolveAttributeConfiguration(propertyDescriptor);
@@ -225,6 +226,14 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
225226
return builder.build();
226227
}
227228

229+
private static String getFlattenedPrefix(PropertyDescriptor propertyDescriptor, DynamoDbFlatten dynamoDbFlatten) {
230+
boolean useAutoPrefix = DynamoDbFlatten.AUTO_PREFIX.equals(dynamoDbFlatten.prefix());
231+
if (!useAutoPrefix) {
232+
return dynamoDbFlatten.prefix();
233+
}
234+
return attributeNameForProperty(propertyDescriptor) + ".";
235+
}
236+
228237
private static AttributeConfiguration resolveAttributeConfiguration(PropertyDescriptor propertyDescriptor) {
229238
boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor,
230239
DynamoDbPreserveEmptyObject.class) != null;

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ private static <T, B> StaticImmutableTableSchema<T, B> createStaticImmutableTab
201201
if (dynamoDbFlatten != null) {
202202
builder.flatten(TableSchema.fromClass(propertyDescriptor.getter().getReturnType()),
203203
getterForProperty(propertyDescriptor, immutableClass),
204-
setterForProperty(propertyDescriptor, builderClass));
204+
setterForProperty(propertyDescriptor, builderClass),
205+
getFlattenedPrefix(propertyDescriptor, dynamoDbFlatten));
205206
} else {
206207
AttributeConfiguration beanAttributeConfiguration = resolveAttributeConfiguration(propertyDescriptor);
207208
ImmutableAttribute.Builder<T, B, ?> attributeBuilder =
@@ -225,6 +226,14 @@ private static <T, B> StaticImmutableTableSchema<T, B> createStaticImmutableTab
225226
return builder.build();
226227
}
227228

229+
private static String getFlattenedPrefix(ImmutablePropertyDescriptor propertyDescriptor, DynamoDbFlatten dynamoDbFlatten) {
230+
boolean useAutoPrefix = DynamoDbFlatten.AUTO_PREFIX.equals(dynamoDbFlatten.prefix());
231+
if (!useAutoPrefix) {
232+
return dynamoDbFlatten.prefix();
233+
}
234+
return attributeNameForProperty(propertyDescriptor) + ".";
235+
}
236+
228237
private static List<AttributeConverterProvider> createConverterProvidersFromAnnotation(Class<?> immutableClass,
229238
DynamoDbImmutable dynamoDbImmutable) {
230239

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbFlatten.java

+48
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,62 @@
2525
* This annotation is used to flatten all the attributes of a separate DynamoDb bean that is stored in the current bean
2626
* object and add them as top level attributes to the record that is read and written to the database. The target bean
2727
* to flatten must be specified as part of this annotation.
28+
* The flattening behavior can be controlled by the prefix value of the annotation.
29+
* The default behavior is that no prefix is applied (this is done for backwards compatability).
30+
* If a String value is supplied then that is prefixed to the attribute names.
31+
* If a value of {@code DynamoDbFlatten.AUTO_PREFIX} is supplied then the attribute name of the flattened bean appended
32+
* with a period ('.') is used as the prefix.
33+
*
34+
* Example, given the following classes:
35+
* <pre>{@code
36+
* @DynamoDbBean
37+
* public class Flattened {
38+
* String getValue();
39+
* }
40+
*
41+
* @DynamoDbBean
42+
* public class Record {
43+
* @DynamoDbFlatten
44+
* Flattened getNoPrefix(); // translates to attribute 'value'
45+
* @DynamoDbFlatten(prefix = "prefix-")
46+
* Flattened getExplicitPrefix(); // translates to attribute 'prefix-value'
47+
* @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
48+
* Flattened getInferredPrefix(); // translates to attribute 'inferredPrefix.value'
49+
* @DynamoDbAttribute("custom")
50+
* @DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
51+
* Flattened getFlattened(); // translates to attribute 'custom.value'
52+
* }
53+
*}</pre>
54+
* They would be mapped as such:
55+
* <pre>{@code
56+
* {
57+
* "value": {"S": "..."},
58+
* "prefix-value": {"S": "..."},
59+
* "inferredPrefix.value": {"S": "..."},
60+
* "custom.value": {"S": "..."},
61+
* }
62+
* }</pre>
2863
*/
2964
@Target({ElementType.METHOD})
3065
@Retention(RetentionPolicy.RUNTIME)
3166
@SdkPublicApi
3267
public @interface DynamoDbFlatten {
68+
/**
69+
* Values used to denote that the mapper should append the current attribute name to flattened fields.
70+
*/
71+
String AUTO_PREFIX = "AUTO_PREFIX";
72+
3373
/**
3474
* @deprecated This is no longer used, the class type of the attribute will be used instead.
3575
*/
3676
@Deprecated
3777
Class<?> dynamoDbBeanClass() default Object.class;
78+
79+
/**
80+
* Optional prefix to append to the flattened bean attributes in the schema.
81+
* Specifying a value of {@code DynamoDbFlatten.AUTO_PREFIX} will use the annotated methods attribute name as the
82+
* prefix.
83+
* default: {@code ""} (No prefix)
84+
*/
85+
String prefix() default "";
3886
}

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java

+49-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.Optional;
3838
import org.junit.Rule;
3939
import org.junit.Test;
40+
import org.junit.jupiter.api.Assertions;
4041
import org.junit.rules.ExpectedException;
4142
import org.junit.runner.RunWith;
4243
import org.mockito.junit.MockitoJUnitRunner;
@@ -54,6 +55,7 @@
5455
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EnumBean;
5556
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ExtendedBean;
5657
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanBean;
58+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanImmutable;
5759
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableBean;
5860
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean;
5961
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean;
@@ -178,18 +180,57 @@ public void dynamoDbAttribute_remapsAttributeName() {
178180
@Test
179181
public void dynamoDbFlatten_correctlyFlattensBeanAttributes() {
180182
BeanTableSchema<FlattenedBeanBean> beanTableSchema = BeanTableSchema.create(FlattenedBeanBean.class);
183+
184+
assertThat(beanTableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2",
185+
"prefix-attribute2", "autoPrefixBean.attribute2", "custom.attribute2"));
186+
181187
AbstractBean abstractBean = new AbstractBean();
182188
abstractBean.setAttribute2("two");
183189
FlattenedBeanBean flattenedBeanBean = new FlattenedBeanBean();
184190
flattenedBeanBean.setId("id-value");
185191
flattenedBeanBean.setAttribute1("one");
186192
flattenedBeanBean.setAbstractBean(abstractBean);
193+
flattenedBeanBean.setExplicitPrefixBean(abstractBean);
194+
flattenedBeanBean.setAutoPrefixBean(abstractBean);
195+
flattenedBeanBean.setCustomPrefixBean(abstractBean);
187196

188197
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedBeanBean, false);
189-
assertThat(itemMap.size(), is(3));
198+
assertThat(itemMap.size(), is(6));
190199
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
191200
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
192201
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
202+
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
203+
assertThat(itemMap, hasEntry("autoPrefixBean.attribute2", stringValue("two")));
204+
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
205+
}
206+
207+
@Test
208+
public void dynamoDbFlatten_correctlyGetFlattenedBeanAttributes() {
209+
BeanTableSchema<FlattenedBeanBean> tableSchema = BeanTableSchema.create(FlattenedBeanBean.class);
210+
211+
AbstractBean abstractBean = new AbstractBean();
212+
abstractBean.setAttribute2("two");
213+
AbstractBean explicitPrefixBean = new AbstractBean();
214+
explicitPrefixBean.setAttribute2("three");
215+
AbstractBean autoPrefixBean = new AbstractBean();
216+
autoPrefixBean.setAttribute2("four");
217+
AbstractBean customPrefixBean = new AbstractBean();
218+
customPrefixBean.setAttribute2("five");
219+
220+
FlattenedBeanBean bean = new FlattenedBeanBean();
221+
bean.setId("id-value");
222+
bean.setAttribute1("one");
223+
bean.setAbstractBean(abstractBean);
224+
bean.setExplicitPrefixBean(explicitPrefixBean);
225+
bean.setAutoPrefixBean(autoPrefixBean);
226+
bean.setCustomPrefixBean(customPrefixBean);
227+
228+
assertThat(tableSchema.attributeValue(bean, "id"), equalTo(stringValue("id-value")));
229+
assertThat(tableSchema.attributeValue(bean, "attribute1"), equalTo(stringValue("one")));
230+
assertThat(tableSchema.attributeValue(bean, "attribute2"), equalTo(stringValue("two")));
231+
assertThat(tableSchema.attributeValue(bean, "prefix-attribute2"), equalTo(stringValue("three")));
232+
assertThat(tableSchema.attributeValue(bean, "autoPrefixBean.attribute2"), equalTo(stringValue("four")));
233+
assertThat(tableSchema.attributeValue(bean, "custom.attribute2"), equalTo(stringValue("five")));
193234
}
194235

195236
@Test
@@ -248,12 +289,18 @@ public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() {
248289
flattenedImmutableBean.setId("id-value");
249290
flattenedImmutableBean.setAttribute1("one");
250291
flattenedImmutableBean.setAbstractImmutable(abstractImmutable);
292+
flattenedImmutableBean.setExplicitPrefixImmutable(abstractImmutable);
293+
flattenedImmutableBean.setAutoPrefixImmutable(abstractImmutable);
294+
flattenedImmutableBean.setCustomPrefixImmutable(abstractImmutable);
251295

252296
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false);
253-
assertThat(itemMap.size(), is(3));
297+
assertThat(itemMap.size(), is(6));
254298
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
255299
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
256300
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
301+
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
302+
assertThat(itemMap, hasEntry("autoPrefixImmutable.attribute2", stringValue("two")));
303+
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
257304
}
258305

259306
@Test

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java

+56-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import static java.util.Collections.singletonMap;
1919
import static org.hamcrest.MatcherAssert.assertThat;
20+
import static org.hamcrest.Matchers.contains;
21+
import static org.hamcrest.Matchers.containsInAnyOrder;
22+
import static org.hamcrest.Matchers.equalTo;
2023
import static org.hamcrest.Matchers.hasEntry;
2124
import static org.hamcrest.Matchers.is;
2225
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue;
@@ -25,6 +28,8 @@
2528
import java.util.Arrays;
2629
import java.util.HashMap;
2730
import java.util.Map;
31+
import java.util.stream.Collectors;
32+
import org.junit.jupiter.api.Assertions;
2833
import org.junit.jupiter.api.Test;
2934
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean;
3035
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable;
@@ -213,37 +218,86 @@ public void documentImmutable_map_correctlyMapsImmutableAttributes() {
213218
public void dynamoDbFlatten_correctlyFlattensBeanAttributes() {
214219
ImmutableTableSchema<FlattenedBeanImmutable> tableSchema =
215220
ImmutableTableSchema.create(FlattenedBeanImmutable.class);
221+
222+
assertThat(tableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2",
223+
"prefix-attribute2", "autoPrefixBean.attribute2", "custom.attribute2"));
224+
216225
AbstractBean abstractBean = new AbstractBean();
217226
abstractBean.setAttribute2("two");
218227
FlattenedBeanImmutable flattenedBeanImmutable =
219228
new FlattenedBeanImmutable.Builder().setId("id-value")
220229
.setAttribute1("one")
221230
.setAbstractBean(abstractBean)
231+
.setExplicitPrefixBean(abstractBean)
232+
.setAutoPrefixBean(abstractBean)
233+
.setCustomPrefixBean(abstractBean)
222234
.build();
223235

224236
Map<String, AttributeValue> itemMap = tableSchema.itemToMap(flattenedBeanImmutable, false);
225-
assertThat(itemMap.size(), is(3));
237+
assertThat(itemMap.size(), is(6));
226238
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
227239
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
228240
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
241+
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
242+
assertThat(itemMap, hasEntry("autoPrefixBean.attribute2", stringValue("two")));
243+
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
229244
}
230245

231246
@Test
232247
public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() {
233248
ImmutableTableSchema<FlattenedImmutableImmutable> tableSchema =
234249
ImmutableTableSchema.create(FlattenedImmutableImmutable.class);
250+
251+
assertThat(tableSchema.attributeNames(), containsInAnyOrder("id", "attribute1", "attribute2",
252+
"prefix-attribute2", "autoPrefixImmutable.attribute2", "custom.attribute2"));
253+
235254
AbstractImmutable abstractImmutable = AbstractImmutable.builder().attribute2("two").build();
236255
FlattenedImmutableImmutable FlattenedImmutableImmutable =
237256
new FlattenedImmutableImmutable.Builder().setId("id-value")
238257
.setAttribute1("one")
239258
.setAbstractImmutable(abstractImmutable)
259+
.setExplicitPrefixImmutable(abstractImmutable)
260+
.setAutoPrefixImmutable(abstractImmutable)
261+
.setCustomPrefixImmutable(abstractImmutable)
240262
.build();
241263

242264
Map<String, AttributeValue> itemMap = tableSchema.itemToMap(FlattenedImmutableImmutable, false);
243-
assertThat(itemMap.size(), is(3));
265+
assertThat(itemMap.size(), is(6));
244266
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
245267
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
246268
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
269+
assertThat(itemMap, hasEntry("prefix-attribute2", stringValue("two")));
270+
assertThat(itemMap, hasEntry("autoPrefixImmutable.attribute2", stringValue("two")));
271+
assertThat(itemMap, hasEntry("custom.attribute2", stringValue("two")));
272+
}
273+
274+
@Test
275+
public void dynamoDbFlatten_correctlyGetFlattenedBeanAttributes() {
276+
ImmutableTableSchema<FlattenedBeanImmutable> tableSchema =
277+
ImmutableTableSchema.create(FlattenedBeanImmutable.class);
278+
279+
AbstractBean abstractBean = new AbstractBean();
280+
abstractBean.setAttribute2("two");
281+
AbstractBean explicitPrefixBean = new AbstractBean();
282+
explicitPrefixBean.setAttribute2("three");
283+
AbstractBean autoPrefixBean = new AbstractBean();
284+
autoPrefixBean.setAttribute2("four");
285+
AbstractBean customPrefixBean = new AbstractBean();
286+
customPrefixBean.setAttribute2("five");
287+
FlattenedBeanImmutable bean = new FlattenedBeanImmutable.Builder().setId("id-value")
288+
.setAttribute1("one")
289+
.setAbstractBean(abstractBean)
290+
.setExplicitPrefixBean(explicitPrefixBean)
291+
.setAutoPrefixBean(autoPrefixBean)
292+
.setCustomPrefixBean(customPrefixBean)
293+
.build();
294+
295+
assertThat(tableSchema.attributeValue(bean, "id"), equalTo(stringValue("id-value")));
296+
assertThat(tableSchema.attributeValue(bean, "attribute1"), equalTo(stringValue("one")));
297+
assertThat(tableSchema.attributeValue(bean, "attribute2"), equalTo(stringValue("two")));
298+
assertThat(tableSchema.attributeValue(bean, "prefix-attribute2"), equalTo(stringValue("three")));
299+
assertThat(tableSchema.attributeValue(bean, "autoPrefixBean.attribute2"), equalTo(stringValue("four")));
300+
assertThat(tableSchema.attributeValue(bean, "custom.attribute2"), equalTo(stringValue("five")));
247301
}
248302

249303
@Test

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/FlattenedBeanBean.java

+29
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

18+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
1819
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
1920
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
2021
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
@@ -24,6 +25,9 @@ public class FlattenedBeanBean {
2425
private String id;
2526
private String attribute1;
2627
private AbstractBean abstractBean;
28+
private AbstractBean explicitPrefixBean;
29+
private AbstractBean autoPrefixBean;
30+
private AbstractBean customPrefixBean;
2731

2832
@DynamoDbPartitionKey
2933
public String getId() {
@@ -47,4 +51,29 @@ public AbstractBean getAbstractBean() {
4751
public void setAbstractBean(AbstractBean abstractBean) {
4852
this.abstractBean = abstractBean;
4953
}
54+
55+
@DynamoDbFlatten(prefix = "prefix-")
56+
public AbstractBean getExplicitPrefixBean() {
57+
return explicitPrefixBean;
58+
}
59+
public void setExplicitPrefixBean(AbstractBean explicitPrefixBean) {
60+
this.explicitPrefixBean = explicitPrefixBean;
61+
}
62+
63+
@DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
64+
public AbstractBean getAutoPrefixBean() {
65+
return autoPrefixBean;
66+
}
67+
public void setAutoPrefixBean(AbstractBean autoPrefixBean) {
68+
this.autoPrefixBean = autoPrefixBean;
69+
}
70+
71+
@DynamoDbAttribute("custom")
72+
@DynamoDbFlatten(prefix = DynamoDbFlatten.AUTO_PREFIX)
73+
public AbstractBean getCustomPrefixBean() {
74+
return customPrefixBean;
75+
}
76+
public void setCustomPrefixBean(AbstractBean customPrefixBean) {
77+
this.customPrefixBean = customPrefixBean;
78+
}
5079
}

0 commit comments

Comments
 (0)