Skip to content

Commit 1ea3c2c

Browse files
committed
Support to flatten a Map into top level attributes of the object
1 parent 96c3f02 commit 1ea3c2c

5 files changed

Lines changed: 102 additions & 21 deletions

File tree

.changes/next-release/feature-AmazonDynamoDBEnhancedClient-22723fc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"type": "feature",
33
"category": "Amazon DynamoDB Enhanced Client",
44
"contributor": "",
5-
"description": "Added the support to flatten a Map into top level attributes of the object"
5+
"description": "Add support for @DynamoDBFlattenMap to flatten a Map to top level attributes of an object"
66
}

services-custom/dynamodb-enhanced/README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -686,9 +686,9 @@ private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
686686
Just as for annotations, you can flatten as many different eligible classes as you like using the
687687
builder pattern.
688688

689-
#### Using composition
689+
### Flattening map attributes
690690

691-
Using composition, the @DynamoDBFlattenMap annotation support to flatten a Map:
691+
When using composition, you can apply the @DynamoDbFlattenMap annotation to flatten a Map into top-level attributes:
692692
```java
693693
@DynamoDbBean
694694
public class Customer {
@@ -707,10 +707,15 @@ public class Customer {
707707

708708
@DynamoDbFlattenMap
709709
public Map<String, String> getDetailsMap() { return this.detailsMap; }
710-
public void setDetailsMap(Map<String, String> record) { this.detailsMap = detailsMap;}
710+
public void setDetailsMap(Map<String, String> detailsMap) { this.detailsMap = detailsMap;}
711711
}
712712
```
713-
You can flatten only one map present on a record, otherwise it will be thrown an exception
713+
> **Constraints**
714+
> - A record can contain at most one `@DynamoDbFlattenMap`. This limit applies across the entire class hierarchy, including any composed or flattened classes.
715+
> - The flattened map must use `String` as both the key and value type (`Map<String, String>`).
716+
> - Attribute names generated from map keys must not conflict with existing attributes on the record. If a conflict is detected, an exception will be thrown.
717+
> - If more than one flattened map is present, an exception will be thrown during schema creation.
718+
714719

715720
Flat map composite classes using StaticTableSchema:
716721

@@ -732,6 +737,6 @@ private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
732737
.setter(Customer::setName))
733738
// Because we are flattening a Map object, we supply a getter and setter so the
734739
// mapper knows how to access it
735-
.flattenMap(Map::detailsMap, Map::detailsMap)
740+
.flattenMap(Map::getDetailsMap, Map::setDetailsMap)
736741
.build();
737742
```

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ private B mapToItem(B thisBuilder,
178178
private Map<String, AttributeValue> itemToMap(T item, boolean ignoreNulls, List<String> attributeNames) {
179179
Map<String, String> mapItem = this.mapItemGetter.apply(item);
180180

181-
Map<String, AttributeValue> result = new HashMap<>();
181+
Map<String, AttributeValue> result = new HashMap<>();
182182

183183
if (mapItem != null) {
184184
mapItem.forEach((key, value) -> {
@@ -250,8 +250,8 @@ private StaticImmutableTableSchema(Builder<T, B> builder) {
250250
);
251251

252252
FlattenedMapperForMaps<T, B> mutableFlattenedMapperForMaps = null;
253-
if(builder.flattenedMap != null) {
254-
mutableFlattenedMapperForMaps= builder.flattenedMap;
253+
if (builder.flattenedMap != null) {
254+
mutableFlattenedMapperForMaps = builder.flattenedMap;
255255
}
256256

257257
// Apply table-tags to table metadata
@@ -572,8 +572,8 @@ public T mapToItem(Map<String, AttributeValue> attributeMap, boolean preserveEmp
572572
}
573573

574574
flattenedAttributeValues.put(key, value.s());
575-
flattenedMapAttributeValues.put(indexedFlattenedMapperForMaps, flattenedAttributeValues);
576-
}
575+
flattenedMapAttributeValues.put(indexedFlattenedMapperForMaps, flattenedAttributeValues);
576+
}
577577
}
578578
}
579579
}

services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/FlattenMapTest.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2020

21+
import java.util.Collections;
2122
import java.util.HashMap;
2223
import java.util.stream.Collectors;
2324
import java.util.stream.Stream;
@@ -30,7 +31,9 @@
3031
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
3132
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
3233
import software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedTimestampRecordExtension;
34+
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FlattenRecord;
3335
import software.amazon.awssdk.enhanced.dynamodb.internal.client.ExtensionResolver;
36+
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.flattenmap.FlattenMapAndFlattenRecordBean;
3437
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.flattenmap.FlattenMapValidBean;
3538

3639
public class FlattenMapTest extends LocalDynamoDbSyncTestBase {
@@ -68,7 +71,7 @@ public void updateItemWithFlattenMap_correctlyFlattensMapAttributes() {
6871

6972
//first update
7073
FlattenMapValidBean record = new FlattenMapValidBean();
71-
record.setId("111");
74+
record.setId("1");
7275
record.setRootAttribute1("rootValue1");
7376
record.setRootAttribute2("rootValue2");
7477
record.setAttributesMap(new HashMap<String, String>() {{
@@ -80,7 +83,7 @@ public void updateItemWithFlattenMap_correctlyFlattensMapAttributes() {
8083
mappedTable.updateItem(record);
8184

8285
FlattenMapValidBean persistedRecord = mappedTable.getItem(record);
83-
assertThat(persistedRecord.getId()).isEqualTo("111");
86+
assertThat(persistedRecord.getId()).isEqualTo("1");
8487
assertThat(persistedRecord.getRootAttribute1()).isEqualTo("rootValue1");
8588
assertThat(persistedRecord.getRootAttribute2()).isEqualTo("rootValue2");
8689
assertThat(persistedRecord.getAttributesMap()).hasSize(3);
@@ -91,7 +94,7 @@ public void updateItemWithFlattenMap_correctlyFlattensMapAttributes() {
9194

9295
//second update
9396
record = new FlattenMapValidBean();
94-
record.setId("222");
97+
record.setId("2");
9598
record.setRootAttribute2("rootValue2_new");
9699
record.setAttributesMap(new HashMap<String, String>() {{
97100
put("mapAttribute1", "mapValue1_new");
@@ -102,7 +105,7 @@ record = new FlattenMapValidBean();
102105
mappedTable.updateItem(record);
103106

104107
persistedRecord = mappedTable.getItem(record);
105-
assertThat(persistedRecord.getId()).isEqualTo("222");
108+
assertThat(persistedRecord.getId()).isEqualTo("2");
106109
assertThat(persistedRecord.getRootAttribute1()).isNull();
107110
assertThat(persistedRecord.getRootAttribute2()).isEqualTo("rootValue2_new");
108111
assertThat(persistedRecord.getAttributesMap()).hasSize(3);
@@ -113,7 +116,7 @@ record = new FlattenMapValidBean();
113116

114117
//third update
115118
record = new FlattenMapValidBean();
116-
record.setId("333");
119+
record.setId("3");
117120
record.setRootAttribute1("rootValue1_new");
118121
record.setRootAttribute2("rootValue2_new");
119122
record.setAttributesMap(new HashMap<String, String>() {{
@@ -125,7 +128,7 @@ record = new FlattenMapValidBean();
125128
mappedTable.updateItem(record);
126129

127130
persistedRecord = mappedTable.getItem(record);
128-
assertThat(persistedRecord.getId()).isEqualTo("333");
131+
assertThat(persistedRecord.getId()).isEqualTo("3");
129132
assertThat(persistedRecord.getRootAttribute1()).isEqualTo("rootValue1_new");
130133
assertThat(persistedRecord.getRootAttribute2()).isEqualTo("rootValue2_new");
131134
assertThat(persistedRecord.getAttributesMap()).hasSize(3);
@@ -136,13 +139,13 @@ record = new FlattenMapValidBean();
136139

137140
//fourth update
138141
record = new FlattenMapValidBean();
139-
record.setId("444");
142+
record.setId("4");
140143
record.setAttributesMap(new HashMap<>());
141144

142145
mappedTable.updateItem(record);
143146

144147
persistedRecord = mappedTable.getItem(record);
145-
assertThat(persistedRecord.getId()).isEqualTo("444");
148+
assertThat(persistedRecord.getId()).isEqualTo("4");
146149
assertThat(persistedRecord.getRootAttribute1()).isNull();
147150
assertThat(persistedRecord.getRootAttribute2()).isNull();
148151
assertThat(persistedRecord.getAttributesMap()).isNull();
@@ -151,7 +154,7 @@ record = new FlattenMapValidBean();
151154
@Test
152155
public void updateItemWithFlattenMap_withDuplicateAttributeName_throwsIllegalArgumentException() {
153156
FlattenMapValidBean record = new FlattenMapValidBean();
154-
record.setId("123");
157+
record.setId("1");
155158
record.setRootAttribute1("rootValue1");
156159
record.setRootAttribute2("rootValue2");
157160
record.setAttributesMap(new HashMap<String, String>() {{
@@ -167,4 +170,29 @@ public void updateItemWithFlattenMap_withDuplicateAttributeName_throwsIllegalArg
167170
.hasMessageContaining("[Attribute name: id]");
168171
}
169172

170-
}
173+
@Test
174+
public void updateItemWithFlattenMap_duplicateAttributeInFlattenedRecord_throwsIllegalArgumentException() {
175+
String composedTableName = getConcreteTableName("table-name-composed");
176+
177+
TableSchema<FlattenMapAndFlattenRecordBean> composedSchema = TableSchema.fromClass(FlattenMapAndFlattenRecordBean.class);
178+
DynamoDbTable<FlattenMapAndFlattenRecordBean> composedTable = enhancedClient.table(composedTableName, composedSchema);
179+
composedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput()));
180+
181+
try {
182+
FlattenRecord flattenRecord = new FlattenRecord();
183+
flattenRecord.setId("1");
184+
185+
FlattenMapAndFlattenRecordBean flattenMapAndFlattenRecord = new FlattenMapAndFlattenRecordBean();
186+
flattenMapAndFlattenRecord.setFlattenRecord(flattenRecord);
187+
flattenMapAndFlattenRecord.setAttributesMap(Collections.singletonMap("id", "id2"));
188+
189+
assertThatThrownBy(() -> composedTable.updateItem(flattenMapAndFlattenRecord))
190+
.isInstanceOf(IllegalArgumentException.class)
191+
.hasMessageContaining("Attempt to add an attribute to a mapper that already has one with the same name. ")
192+
.hasMessageContaining("[Attribute name: id]");
193+
194+
} finally {
195+
getDynamoDbClient().deleteTable(r -> r.tableName(composedTableName));
196+
}
197+
}
198+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.mapper.testbeans.flattenmap;
17+
18+
import java.util.Map;
19+
import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
20+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
21+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlattenMap;
22+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
23+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
24+
25+
@DynamoDbBean
26+
public class FlattenMapWithUpdateBehaviorBean {
27+
private String id;
28+
private Map<String, String> attributesMap;
29+
30+
@DynamoDbPartitionKey
31+
public String getId() {
32+
return id;
33+
}
34+
35+
public void setId(String id) {
36+
this.id = id;
37+
}
38+
39+
@DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
40+
@DynamoDbFlattenMap
41+
public Map<String, String> getAttributesMap() {
42+
return attributesMap;
43+
}
44+
45+
public void setAttributesMap(Map<String, String> attributesMap) {
46+
this.attributesMap = attributesMap;
47+
}
48+
}

0 commit comments

Comments
 (0)