Skip to content

Commit f95780f

Browse files
committed
Fixed DynamoDbEnhancedClient TableSchema::itemToMap to handle null flattened members when ignoreNulls is false
1 parent 1d6bb4c commit f95780f

File tree

7 files changed

+498
-5
lines changed

7 files changed

+498
-5
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"description": "Fixed DynamoDbEnhancedClient TableSchema::itemToMap to return a map that contains a consistent representation of null top-level (non-flattened) attributes and flattened attributes when their enclosing member is null and ignoreNulls is set to false."
6+
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private B mapToItem(B thisBuilder,
127127
private Map<String, AttributeValue> itemToMap(T item, boolean ignoreNulls) {
128128
T1 otherItem = this.otherItemGetter.apply(item);
129129

130-
if (otherItem == null) {
130+
if (otherItem == null && ignoreNulls) {
131131
return Collections.emptyMap();
132132
}
133133

@@ -612,15 +612,21 @@ public Map<String, AttributeValue> itemToMap(T item, boolean ignoreNulls) {
612612

613613
attributeMappers.forEach(attributeMapper -> {
614614
String attributeKey = attributeMapper.attributeName();
615-
AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item);
615+
AttributeValue attributeValue = item == null ?
616+
AttributeValue.fromNul(true) :
617+
attributeMapper.attributeGetterMethod().apply(item);
616618

617619
if (!ignoreNulls || !isNullAttributeValue(attributeValue)) {
618620
attributeValueMap.put(attributeKey, attributeValue);
619621
}
620622
});
621623

622624
flattenedObjectMappers.forEach((name, flattenedMapper) -> {
623-
attributeValueMap.putAll(flattenedMapper.itemToMap(item, ignoreNulls));
625+
if (item != null) {
626+
attributeValueMap.putAll(flattenedMapper.itemToMap(item, ignoreNulls));
627+
} else if (!ignoreNulls) {
628+
attributeValueMap.put(name, AttributeValue.fromNul(true));
629+
}
624630
});
625631

626632
if (flattenedMapMapper != null) {

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

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@
4747
import software.amazon.awssdk.core.SdkBytes;
4848
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
4949
import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext;
50-
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean;
51-
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable;
5250
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
5351
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
5452
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
53+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean;
54+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable;
55+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractNestedImmutable;
5556
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterBean;
5657
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterNoConstructorBean;
5758
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CommonTypesBean;
@@ -62,7 +63,10 @@
6263
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EnumBean;
6364
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ExtendedBean;
6465
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanBean;
66+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedFirstNestedBean;
67+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedFirstNestedBean.FlattenedSecondNestedBean;
6568
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableBean;
69+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedNestedImmutableBean;
6670
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FluentSetterBean;
6771
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean;
6872
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean;
@@ -276,6 +280,128 @@ public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() {
276280
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
277281
}
278282

283+
@Test
284+
public void dynamoDbFlatten_correctlyFlattensNullImmutableAttributes() {
285+
BeanTableSchema<FlattenedImmutableBean> beanTableSchema = BeanTableSchema.create(FlattenedImmutableBean.class);
286+
AbstractImmutable abstractImmutable = AbstractImmutable.builder().build();
287+
FlattenedImmutableBean flattenedImmutableBean = new FlattenedImmutableBean();
288+
flattenedImmutableBean.setId("id-value");
289+
flattenedImmutableBean.setAbstractImmutable(abstractImmutable);
290+
291+
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false);
292+
assertThat(itemMap.size(), is(3));
293+
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
294+
assertThat(itemMap, hasEntry("attribute1", AttributeValue.fromNul(true)));
295+
assertThat(itemMap, hasEntry("attribute2", AttributeValue.fromNul(true)));
296+
}
297+
298+
@Test
299+
public void dynamoDbFlatten_correctlyFlattensNestedImmutableAttributes() {
300+
BeanTableSchema<FlattenedNestedImmutableBean> beanTableSchema =
301+
BeanTableSchema.create(FlattenedNestedImmutableBean.class);
302+
AbstractNestedImmutable abstractNestedImmutable2 =
303+
AbstractNestedImmutable.builder().attribute2("nested-two").build();
304+
AbstractNestedImmutable abstractNestedImmutable1 =
305+
AbstractNestedImmutable.builder().attribute2("two").abstractNestedImmutableOne(abstractNestedImmutable2).build();
306+
FlattenedNestedImmutableBean flattenedNestedImmutableBean = new FlattenedNestedImmutableBean();
307+
flattenedNestedImmutableBean.setId("id-value");
308+
flattenedNestedImmutableBean.setAttribute1("one");
309+
flattenedNestedImmutableBean.setAbstractNestedImmutable(abstractNestedImmutable1);
310+
311+
Map<String, AttributeValue> nestedAttributesMap = new HashMap<>();
312+
nestedAttributesMap.put("abstractNestedImmutableOne", AttributeValue.fromNul(true));
313+
nestedAttributesMap.put("attribute2", stringValue("nested-two"));
314+
315+
AttributeValue expectedNestedAttribute =
316+
AttributeValue.builder().m(Collections.unmodifiableMap(nestedAttributesMap)).build();
317+
318+
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedNestedImmutableBean, false);
319+
assertThat(itemMap.size(), is(4));
320+
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
321+
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
322+
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
323+
assertThat(itemMap, hasEntry("abstractNestedImmutableOne", expectedNestedAttribute));
324+
}
325+
326+
@Test
327+
public void dynamoDbFlatten_correctlyFlattensNullNestedImmutableAttributes() {
328+
BeanTableSchema<FlattenedNestedImmutableBean> beanTableSchema =
329+
BeanTableSchema.create(FlattenedNestedImmutableBean.class);
330+
AbstractNestedImmutable abstractNestedImmutable = AbstractNestedImmutable.builder().build();
331+
FlattenedNestedImmutableBean flattenedNestedImmutableBean = new FlattenedNestedImmutableBean();
332+
flattenedNestedImmutableBean.setId("id-value");
333+
flattenedNestedImmutableBean.setAttribute1("one");
334+
flattenedNestedImmutableBean.setAbstractNestedImmutable(abstractNestedImmutable);
335+
336+
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedNestedImmutableBean, false);
337+
assertThat(itemMap.size(), is(4));
338+
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
339+
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
340+
assertThat(itemMap, hasEntry("attribute2", AttributeValue.fromNul(true)));
341+
assertThat(itemMap, hasEntry("abstractNestedImmutableOne", AttributeValue.fromNul(true)));
342+
}
343+
344+
@Test
345+
public void dynamoDbFlatten_correctlyFlattensSecondNullNestedAttributes_IgnoreNullsFalse() {
346+
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
347+
BeanTableSchema.create(FlattenedFirstNestedBean.class);
348+
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
349+
flattenedFirstNestedBean.setId("id-value");
350+
351+
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, false);
352+
assertThat(itemMap.size(), is(4));
353+
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
354+
assertThat(itemMap, hasEntry("secondId", AttributeValue.fromNul(true)));
355+
assertThat(itemMap, hasEntry("thirdId", AttributeValue.fromNul(true)));
356+
assertThat(itemMap, hasEntry("flattenedFourthBean", AttributeValue.fromNul(true)));
357+
}
358+
359+
@Test
360+
public void dynamoDbFlatten_correctlyFlattensSecondNullNestedAttributes_IgnoreNullsTrue() {
361+
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
362+
BeanTableSchema.create(FlattenedFirstNestedBean.class);
363+
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
364+
flattenedFirstNestedBean.setId("id-value");
365+
366+
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, true);
367+
assertThat(itemMap.size(), is(1));
368+
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
369+
}
370+
371+
@Test
372+
public void dynamoDbFlatten_correctlyFlattensThirdNullNestedAttributes_IgnoreNullsFalse() {
373+
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
374+
BeanTableSchema.create(FlattenedFirstNestedBean.class);
375+
FlattenedSecondNestedBean flattenedSecondNestedBean = new FlattenedSecondNestedBean();
376+
flattenedSecondNestedBean.setSecondId("second-id-value");
377+
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
378+
flattenedFirstNestedBean.setId("id-value");
379+
flattenedFirstNestedBean.setFlattenedSecondNestedBean(flattenedSecondNestedBean);
380+
381+
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, false);
382+
assertThat(itemMap.size(), is(4));
383+
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
384+
assertThat(itemMap, hasEntry("secondId", stringValue("second-id-value")));
385+
assertThat(itemMap, hasEntry("thirdId", AttributeValue.fromNul(true)));
386+
assertThat(itemMap, hasEntry("flattenedFourthBean", AttributeValue.fromNul(true)));
387+
}
388+
389+
@Test
390+
public void dynamoDbFlatten_correctlyFlattensThirdNullNestedAttributes_IgnoreNullsTrue() {
391+
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
392+
BeanTableSchema.create(FlattenedFirstNestedBean.class);
393+
FlattenedSecondNestedBean flattenedSecondNestedBean = new FlattenedSecondNestedBean();
394+
flattenedSecondNestedBean.setSecondId("second-id-value");
395+
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
396+
flattenedFirstNestedBean.setId("id-value");
397+
flattenedFirstNestedBean.setFlattenedSecondNestedBean(flattenedSecondNestedBean);
398+
399+
Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, true);
400+
assertThat(itemMap.size(), is(2));
401+
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
402+
assertThat(itemMap, hasEntry("secondId", stringValue("second-id-value")));
403+
}
404+
279405
@Test
280406
public void documentBean_correctlyMapsBeanAttributes() {
281407
BeanTableSchema<DocumentBean> beanTableSchema = BeanTableSchema.create(DocumentBean.class);

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

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem;
6060
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemComposedClass;
6161
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort;
62+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
6263
import software.amazon.awssdk.enhanced.dynamodb.mapper.testimmutables.EntityEnvelopeImmutable;
6364
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
6465

@@ -782,6 +783,62 @@ public Consumer<StaticTableMetadata.Builder> modifyMetadata() {
782783
}
783784
}
784785

786+
public static final class FakeMappedItemWithDeep {
787+
private String documentString;
788+
private FakeDocumentWithDeep aFakeDocument;
789+
790+
public String getDocumentString() {
791+
return documentString;
792+
}
793+
794+
public void setDocumentString(String documentString) {
795+
this.documentString = documentString;
796+
}
797+
798+
@DynamoDbFlatten
799+
public FakeDocumentWithDeep getAFakeDocument() {
800+
return aFakeDocument;
801+
}
802+
803+
public void setAFakeDocument(FakeDocumentWithDeep aFakeDocument) {
804+
this.aFakeDocument = aFakeDocument;
805+
}
806+
}
807+
808+
public static final class FakeDocumentWithDeep {
809+
private Integer documentInteger;
810+
private DeepFakeDocument deepFakeDocument;
811+
812+
public Integer getDocumentInteger() {
813+
return documentInteger;
814+
}
815+
816+
public void setDocumentInteger(Integer documentInteger) {
817+
this.documentInteger = documentInteger;
818+
}
819+
820+
@DynamoDbFlatten
821+
public DeepFakeDocument getDeepFakeDocument() {
822+
return deepFakeDocument;
823+
}
824+
825+
public void setDeepFakeDocument(DeepFakeDocument deepFakeDocument) {
826+
this.deepFakeDocument = deepFakeDocument;
827+
}
828+
}
829+
830+
public static final class DeepFakeDocument {
831+
private String deepString;
832+
833+
public String getDeepString() {
834+
return deepString;
835+
}
836+
837+
public void setDeepString(String deepString) {
838+
this.deepString = deepString;
839+
}
840+
}
841+
785842
@Mock
786843
private AttributeConverterProvider provider1;
787844

@@ -1388,6 +1445,67 @@ public void buildAbstractWithFlatten() {
13881445
is(singletonMap("documentString", AttributeValue.builder().s("test-string").build())));
13891446
}
13901447

1448+
@Test
1449+
public void buildAbstractWithFlattenAndIgnoreNullAsFalse() {
1450+
StaticTableSchema<FakeMappedItem> tableSchema =
1451+
StaticTableSchema.builder(FakeMappedItem.class)
1452+
.flatten(FAKE_DOCUMENT_TABLE_SCHEMA,
1453+
FakeMappedItem::getAFakeDocument,
1454+
FakeMappedItem::setAFakeDocument)
1455+
.build();
1456+
1457+
FakeDocument document = FakeDocument.of("test-string", null);
1458+
FakeMappedItem item = FakeMappedItem.builder().aFakeDocument(document).build();
1459+
1460+
Map<String, AttributeValue> attributeMapWithNulls = tableSchema.itemToMap(item, false);
1461+
assertThat(attributeMapWithNulls.size(), is(2));
1462+
assertThat(attributeMapWithNulls, hasEntry("documentString", AttributeValue.builder().s("test-string").build()));
1463+
assertThat(attributeMapWithNulls, hasEntry("documentInteger", AttributeValue.fromNul(true)));
1464+
}
1465+
1466+
@Test
1467+
public void buildAbstractWithNestedFlattenAndIgnoreNullAsFalse() {
1468+
StaticTableSchema<DeepFakeDocument> deepSchema =
1469+
StaticTableSchema.builder(DeepFakeDocument.class)
1470+
.newItemSupplier(DeepFakeDocument::new)
1471+
.addAttribute(String.class, a -> a.name("deepString")
1472+
.getter(DeepFakeDocument::getDeepString)
1473+
.setter(DeepFakeDocument::setDeepString))
1474+
.build();
1475+
1476+
StaticTableSchema<FakeDocumentWithDeep> nestedSchema =
1477+
StaticTableSchema.builder(FakeDocumentWithDeep.class)
1478+
.newItemSupplier(FakeDocumentWithDeep::new)
1479+
.addAttribute(Integer.class, a -> a.name("documentInteger")
1480+
.getter(FakeDocumentWithDeep::getDocumentInteger)
1481+
.setter(FakeDocumentWithDeep::setDocumentInteger))
1482+
.flatten(deepSchema,
1483+
FakeDocumentWithDeep::getDeepFakeDocument,
1484+
FakeDocumentWithDeep::setDeepFakeDocument)
1485+
.build();
1486+
1487+
StaticTableSchema<FakeMappedItemWithDeep> tableSchema =
1488+
StaticTableSchema.builder(FakeMappedItemWithDeep.class)
1489+
.newItemSupplier(FakeMappedItemWithDeep::new)
1490+
.addAttribute(String.class, a -> a.name("documentString")
1491+
.getter(FakeMappedItemWithDeep::getDocumentString)
1492+
.setter(FakeMappedItemWithDeep::setDocumentString))
1493+
.flatten(nestedSchema,
1494+
FakeMappedItemWithDeep::getAFakeDocument,
1495+
FakeMappedItemWithDeep::setAFakeDocument)
1496+
.build();
1497+
1498+
FakeMappedItemWithDeep item = new FakeMappedItemWithDeep();
1499+
item.setDocumentString("top-level-string");
1500+
1501+
Map<String, AttributeValue> attributeMap = tableSchema.itemToMap(item, false);
1502+
1503+
assertThat(attributeMap.size(), is(3));
1504+
assertThat(attributeMap, hasEntry("documentString", AttributeValue.builder().s("top-level-string").build()));
1505+
assertThat(attributeMap, hasEntry("documentInteger", AttributeValue.fromNul(true)));
1506+
assertThat(attributeMap, hasEntry("deepString", AttributeValue.fromNul(true)));
1507+
}
1508+
13911509
@Test
13921510
public void buildAbstractExtends() {
13931511
StaticTableSchema<FakeAbstractSuperclass> superclassTableSchema =

0 commit comments

Comments
 (0)