Skip to content

Commit 80004fd

Browse files
committed
DDBEnhanced - Support to flatten a Map into top level attributes of the object
1 parent 8450448 commit 80004fd

File tree

15 files changed

+993
-18
lines changed

15 files changed

+993
-18
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"description": "Added the support to flatten a Map into top level attributes of the object"
6+
}

services-custom/dynamodb-enhanced/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,53 @@ private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
682682
```
683683
Just as for annotations, you can flatten as many different eligible classes as you like using the
684684
builder pattern.
685+
686+
#### Using composition
687+
688+
Using composition, the @DynamoDBFlattenMap annotation support to flatten a Map:
689+
```java
690+
@DynamoDbBean
691+
public class Customer {
692+
private String name;
693+
private String city;
694+
private String address;
695+
696+
private Map<String, String> detailsMap;
697+
698+
public String getName() { return this.name; }
699+
public void setName(String name) { this.name = name;}
700+
public String getCity() { return this.city; }
701+
public void setCity(String city) { this.city = city;}
702+
public String getAddress() { return this.address; }
703+
public void setAddress(String address) { this.name = address;}
704+
705+
@DynamoDbFlattenMap
706+
public Map<String, String> getDetailsMap() { return this.detailsMap; }
707+
public void setDetailsMap(Map<String, String> record) { this.detailsMap = detailsMap;}
708+
}
709+
```
710+
You can flatten only one map present on a record, otherwise it will be thrown an exception
711+
712+
Flat map composite classes using StaticTableSchema:
713+
714+
```java
715+
@Data
716+
public class Customer {
717+
private String name;
718+
private String city;
719+
private String address;
720+
private Map<String, String> detailsMap;
721+
//getters and setters for all attributes
722+
}
723+
724+
private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
725+
StaticTableSchema.builder(Customer.class)
726+
.newItemSupplier(Customer::new)
727+
.addAttribute(String.class, a -> a.name("name")
728+
.getter(Customer::getName)
729+
.setter(Customer::setName))
730+
// Because we are flattening a Map object, we supply a getter and setter so the
731+
// mapper knows how to access it
732+
.flattenMap(Map::detailsMap, Map::detailsMap)
733+
.build();
734+
```

services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedIntegrationTestBase.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey;
2222

2323
import java.util.Arrays;
24+
import java.util.HashMap;
2425
import java.util.List;
2526
import java.util.UUID;
2627
import java.util.stream.Collectors;
@@ -75,6 +76,36 @@ protected static DynamoDbAsyncClient createAsyncDynamoDbClient() {
7576
.setter(Record::setStringAttribute))
7677
.build();
7778

79+
protected static final TableSchema<Record> RECORD_WITH_FLATTEN_MAP_TABLE_SCHEMA =
80+
StaticTableSchema.builder(Record.class)
81+
.newItemSupplier(Record::new)
82+
.addAttribute(String.class, a -> a.name("id")
83+
.getter(Record::getId)
84+
.setter(Record::setId)
85+
.tags(primaryPartitionKey(), secondaryPartitionKey("index1")))
86+
.addAttribute(Integer.class, a -> a.name("sort")
87+
.getter(Record::getSort)
88+
.setter(Record::setSort)
89+
.tags(primarySortKey(), secondarySortKey("index1")))
90+
.addAttribute(Integer.class, a -> a.name("value")
91+
.getter(Record::getValue)
92+
.setter(Record::setValue))
93+
.addAttribute(String.class, a -> a.name("gsi_id")
94+
.getter(Record::getGsiId)
95+
.setter(Record::setGsiId)
96+
.tags(secondaryPartitionKey("gsi_keys_only")))
97+
.addAttribute(Integer.class, a -> a.name("gsi_sort")
98+
.getter(Record::getGsiSort)
99+
.setter(Record::setGsiSort)
100+
.tags(secondarySortKey("gsi_keys_only")))
101+
.addAttribute(String.class, a -> a.name("stringAttribute")
102+
.getter(Record::getStringAttribute)
103+
.setter(Record::setStringAttribute))
104+
.flatten("attributesMap",
105+
Record::getAttributesMap,
106+
Record::setAttributesMap)
107+
.build();
108+
78109

79110
protected static final List<Record> RECORDS =
80111
IntStream.range(0, 9)
@@ -87,6 +118,22 @@ protected static DynamoDbAsyncClient createAsyncDynamoDbClient() {
87118
.setGsiSort(i))
88119
.collect(Collectors.toList());
89120

121+
protected static final List<Record> RECORDS_WITH_FLATTEN_MAP =
122+
IntStream.range(0, 9)
123+
.mapToObj(i -> new Record()
124+
.setId("id-value")
125+
.setSort(i)
126+
.setValue(i)
127+
.setStringAttribute(getStringAttrValue(10 * 1024))
128+
.setGsiId("gsi-id-value")
129+
.setGsiSort(i)
130+
.setAttributesMap(new HashMap<String, String>() {{
131+
put("mapAttribute1", "mapValue1");
132+
put("mapAttribute2", "mapValue2");
133+
put("mapAttribute3", "mapValue3");
134+
}}))
135+
.collect(Collectors.toList());
136+
90137
protected static final List<Record> KEYS_ONLY_RECORDS =
91138
RECORDS.stream()
92139
.map(record -> new Record()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb;
17+
18+
import static org.hamcrest.MatcherAssert.assertThat;
19+
import static org.hamcrest.Matchers.equalTo;
20+
import static org.hamcrest.Matchers.is;
21+
import static org.hamcrest.Matchers.nullValue;
22+
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue;
23+
import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue;
24+
import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBetween;
25+
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.Iterator;
29+
import java.util.List;
30+
import java.util.Map;
31+
import org.junit.AfterClass;
32+
import org.junit.BeforeClass;
33+
import org.junit.Test;
34+
import software.amazon.awssdk.enhanced.dynamodb.model.Page;
35+
import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest;
36+
import software.amazon.awssdk.enhanced.dynamodb.model.Record;
37+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
38+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
39+
40+
public class ScanQueryWithFlattenMapIntegrationTest extends DynamoDbEnhancedIntegrationTestBase {
41+
42+
private static final String TABLE_NAME = createTestTableName();
43+
44+
private static DynamoDbClient dynamoDbClient;
45+
private static DynamoDbEnhancedClient enhancedClient;
46+
private static DynamoDbTable<Record> mappedTable;
47+
48+
@BeforeClass
49+
public static void setup() {
50+
dynamoDbClient = createDynamoDbClient();
51+
enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build();
52+
mappedTable = enhancedClient.table(TABLE_NAME, RECORD_WITH_FLATTEN_MAP_TABLE_SCHEMA);
53+
mappedTable.createTable();
54+
dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME));
55+
}
56+
57+
@AfterClass
58+
public static void teardown() {
59+
try {
60+
dynamoDbClient.deleteTable(r -> r.tableName(TABLE_NAME));
61+
} finally {
62+
dynamoDbClient.close();
63+
}
64+
}
65+
66+
private void insertRecords() {
67+
RECORDS_WITH_FLATTEN_MAP.forEach(record -> mappedTable.putItem(r -> r.item(record)));
68+
}
69+
70+
@Test
71+
public void queryWithFlattenMapRecord_correctlyRetrievesProjectedAttributes() {
72+
insertRecords();
73+
74+
Iterator<Page<Record>> results =
75+
mappedTable.query(QueryEnhancedRequest.builder()
76+
.queryConditional(sortBetween(k-> k.partitionValue("id-value").sortValue(2),
77+
k-> k.partitionValue("id-value").sortValue(6)))
78+
.attributesToProject("mapAttribute1", "mapAttribute2")
79+
.limit(3)
80+
.build())
81+
.iterator();
82+
83+
Page<Record> page1 = results.next();
84+
assertThat(results.hasNext(), is(true));
85+
Page<Record> page2 = results.next();
86+
assertThat(results.hasNext(), is(false));
87+
88+
Map<String, String> resultedAttributesMap = new HashMap<>();
89+
resultedAttributesMap.put("mapAttribute1", "mapValue1");
90+
resultedAttributesMap.put("mapAttribute2", "mapValue2");
91+
92+
List<Record> page1Items = page1.items();
93+
assertThat(page1Items.size(), is(3));
94+
assertThat(page1Items.get(0).getAttributesMap(), is(resultedAttributesMap));
95+
assertThat(page1Items.get(1).getAttributesMap(), is(resultedAttributesMap));
96+
assertThat(page1Items.get(2).getAttributesMap(), is(resultedAttributesMap));
97+
assertThat(page1.consumedCapacity(), is(nullValue()));
98+
assertThat(page1.lastEvaluatedKey(), is(getKeyMap(4)));
99+
assertThat(page1.count(), equalTo(3));
100+
assertThat(page1.scannedCount(), equalTo(3));
101+
102+
List<Record> page2Items = page2.items();
103+
assertThat(page2Items.size(), is(2));
104+
assertThat(page2Items.get(0).getAttributesMap(), is(resultedAttributesMap));
105+
assertThat(page2Items.get(1).getAttributesMap(), is(resultedAttributesMap));
106+
assertThat(page2.lastEvaluatedKey(), is(nullValue()));
107+
assertThat(page2.count(), equalTo(2));
108+
assertThat(page2.scannedCount(), equalTo(2));
109+
}
110+
111+
private Map<String, AttributeValue> getKeyMap(int sort) {
112+
Map<String, AttributeValue> result = new HashMap<>();
113+
result.put("id", stringValue(RECORDS.get(sort).getId()));
114+
result.put("sort", numberValue(RECORDS.get(sort).getSort()));
115+
return Collections.unmodifiableMap(result);
116+
}
117+
}

services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/Record.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb.model;
1717

18+
import java.util.Map;
1819
import java.util.Objects;
1920

2021
public class Record {
@@ -27,6 +28,8 @@ public class Record {
2728

2829
private String stringAttribute;
2930

31+
private Map<String, String> attributesMap;
32+
3033
public String getId() {
3134
return id;
3235
}
@@ -81,6 +84,15 @@ public Record setStringAttribute(String stringAttribute) {
8184
return this;
8285
}
8386

87+
public Map<String, String> getAttributesMap() {
88+
return attributesMap;
89+
}
90+
91+
public Record setAttributesMap(Map<String, String> attributesMap) {
92+
this.attributesMap = attributesMap;
93+
return this;
94+
}
95+
8496
@Override
8597
public boolean equals(Object o) {
8698
if (this == o) return true;
@@ -91,11 +103,12 @@ public boolean equals(Object o) {
91103
Objects.equals(value, record.value) &&
92104
Objects.equals(gsiId, record.gsiId) &&
93105
Objects.equals(stringAttribute, record.stringAttribute) &&
94-
Objects.equals(gsiSort, record.gsiSort);
106+
Objects.equals(gsiSort, record.gsiSort) &&
107+
Objects.equals(attributesMap, record.attributesMap);
95108
}
96109

97110
@Override
98111
public int hashCode() {
99-
return Objects.hash(id, sort, value, gsiId, gsiSort, stringAttribute);
112+
return Objects.hash(id, sort, value, gsiId, gsiSort, stringAttribute, attributesMap);
100113
}
101114
}

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

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.Collections;
3636
import java.util.List;
3737
import java.util.Map;
38+
import java.util.Objects;
3839
import java.util.Optional;
3940
import java.util.WeakHashMap;
4041
import java.util.function.BiConsumer;
@@ -63,6 +64,7 @@
6364
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
6465
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
6566
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
67+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlattenMap;
6668
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore;
6769
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls;
6870
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable;
@@ -222,6 +224,14 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
222224
throw new IllegalArgumentException(e);
223225
}
224226

227+
List<PropertyDescriptor> mappableProperties = Arrays.stream(beanInfo.getPropertyDescriptors())
228+
.filter(p -> isMappableProperty(beanClass, p))
229+
.collect(Collectors.toList());
230+
231+
if (dynamoDbFlattenMapAnnotationHasInvalidUse(mappableProperties)) {
232+
throw new IllegalArgumentException("More than one @DynamoDbFlattenMap annotation found on the same record");
233+
}
234+
225235
Supplier<T> newObjectSupplier = newObjectSupplierForClass(beanClass, lookup);
226236

227237
StaticTableSchema.Builder<T> builder = StaticTableSchema.builder(beanClass)
@@ -231,29 +241,35 @@ private static <T> StaticTableSchema<T> createStaticTableSchema(Class<T> beanCla
231241

232242
List<StaticAttribute<T, ?>> attributes = new ArrayList<>();
233243

234-
Arrays.stream(beanInfo.getPropertyDescriptors())
235-
.filter(p -> isMappableProperty(beanClass, p))
236-
.forEach(propertyDescriptor -> {
244+
mappableProperties.forEach(propertyDescriptor -> {
237245
DynamoDbFlatten dynamoDbFlatten = getPropertyAnnotation(propertyDescriptor, DynamoDbFlatten.class);
238246

239247
if (dynamoDbFlatten != null) {
240248
builder.flatten(TableSchema.fromClass(propertyDescriptor.getReadMethod().getReturnType()),
241249
getterForProperty(propertyDescriptor, beanClass, lookup),
242250
setterForProperty(propertyDescriptor, beanClass, lookup));
243251
} else {
244-
AttributeConfiguration attributeConfiguration =
245-
resolveAttributeConfiguration(propertyDescriptor);
252+
DynamoDbFlattenMap dynamoDbFlattenMap = getPropertyAnnotation(propertyDescriptor, DynamoDbFlattenMap.class);
253+
254+
if (dynamoDbFlattenMap != null) {
255+
builder.flatten(propertyDescriptor.getName(), getterForProperty(propertyDescriptor, beanClass, lookup),
256+
setterForProperty(propertyDescriptor, beanClass, lookup));
246257

247-
StaticAttribute.Builder<T, ?> attributeBuilder =
248-
staticAttributeBuilder(propertyDescriptor, beanClass, lookup, metaTableSchemaCache,
249-
attributeConfiguration);
258+
} else {
259+
AttributeConfiguration attributeConfiguration =
260+
resolveAttributeConfiguration(propertyDescriptor);
250261

251-
Optional<AttributeConverter> attributeConverter =
262+
StaticAttribute.Builder<T, ?> attributeBuilder =
263+
staticAttributeBuilder(propertyDescriptor, beanClass, lookup, metaTableSchemaCache,
264+
attributeConfiguration);
265+
266+
Optional<AttributeConverter> attributeConverter =
252267
createAttributeConverterFromAnnotation(propertyDescriptor, lookup);
253-
attributeConverter.ifPresent(attributeBuilder::attributeConverter);
268+
attributeConverter.ifPresent(attributeBuilder::attributeConverter);
254269

255-
addTagsToAttribute(attributeBuilder, propertyDescriptor);
256-
attributes.add(attributeBuilder.build());
270+
addTagsToAttribute(attributeBuilder, propertyDescriptor);
271+
attributes.add(attributeBuilder.build());
272+
}
257273
}
258274
});
259275

@@ -286,6 +302,15 @@ private static Optional<Method> findFluentSetter(Class<?> beanClass, String prop
286302
.findFirst();
287303
}
288304

305+
private static boolean dynamoDbFlattenMapAnnotationHasInvalidUse(List<PropertyDescriptor> mappableProperties) {
306+
return mappableProperties.stream()
307+
.map(pd -> getPropertyAnnotation(pd, DynamoDbFlattenMap.class))
308+
.filter(Objects::nonNull)
309+
.skip(1)
310+
.findFirst()
311+
.isPresent();
312+
}
313+
289314
private static AttributeConfiguration resolveAttributeConfiguration(PropertyDescriptor propertyDescriptor) {
290315
boolean shouldPreserveEmptyObject = getPropertyAnnotation(propertyDescriptor,
291316
DynamoDbPreserveEmptyObject.class) != null;

0 commit comments

Comments
 (0)