Skip to content

Commit 79394aa

Browse files
authored
Fixed EnhancedClient UpdateItem operation to make it work on nested attributes as well (#5380)
* Fixed EnhancedClient UpdateItem operation to make it work on nested attributes as well * Add Tests for multi-level nesting updates to work correctly * Fixed PR feedback * Updated Javadocs * Addressed Pr feedback * Removed indendation changes * Added tests for FlattenedMapper * fixed indendation * Fix indendation and remove unintentional changes * Configured MappingConfiguration object * Added methods to AttributeMapping interface * Fixed unintentional indendation changes * Fixed unintentional indendation changes * Add changelogs * Introduce a new method to transform input to be able to perform update operations on nested DynamoDB object attributes. * Remove unwanted changes * Indent * Remove unwanted changes * Added testing * Added test to validate updating string to null * Remove unintentional indentation changes * Fix checkstyle * Update test case to add empty nested attribute * Added a test to verify that updates to non-scalar nested attributes with ignoreNulls set to true, throws DDBException * Uncomment test assertions * Ensure correct workings of updating to an emoty map * Fixed checkstyle * Addressed pr feedback
1 parent 017292e commit 79394aa

File tree

9 files changed

+486
-9
lines changed

9 files changed

+486
-9
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": "AWS SDK for Java v2",
4+
"contributor": "anirudh9391",
5+
"description": "Introduce a new method to transform input to be able to perform update operations on nested DynamoDB object attributes."
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/EnhancedClientUtils.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515

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

18+
import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE;
19+
1820
import java.util.Collections;
1921
import java.util.List;
2022
import java.util.Map;
2123
import java.util.Optional;
2224
import java.util.Set;
2325
import java.util.function.Function;
2426
import java.util.function.Supplier;
27+
import java.util.regex.Pattern;
2528
import java.util.stream.Collectors;
2629
import java.util.stream.Stream;
2730
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -40,6 +43,7 @@ public final class EnhancedClientUtils {
4043
private static final Set<Character> SPECIAL_CHARACTERS = Stream.of(
4144
'*', '.', '-', '#', '+', ':', '/', '(', ')', ' ',
4245
'&', '<', '>', '?', '=', '!', '@', '%', '$', '|').collect(Collectors.toSet());
46+
private static final Pattern NESTED_OBJECT_PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE);
4347

4448
private EnhancedClientUtils() {
4549

@@ -67,18 +71,30 @@ public static String cleanAttributeName(String key) {
6771
return somethingChanged ? new String(chars) : key;
6872
}
6973

74+
private static boolean isNestedAttribute(String key) {
75+
return key.contains(NESTED_OBJECT_UPDATE);
76+
}
77+
7078
/**
7179
* Creates a key token to be used with an ExpressionNames map.
7280
*/
7381
public static String keyRef(String key) {
74-
return "#AMZN_MAPPED_" + cleanAttributeName(key);
82+
String cleanAttributeName = cleanAttributeName(key);
83+
cleanAttributeName = isNestedAttribute(cleanAttributeName) ?
84+
NESTED_OBJECT_PATTERN.matcher(cleanAttributeName).replaceAll(".#AMZN_MAPPED_")
85+
: cleanAttributeName;
86+
return "#AMZN_MAPPED_" + cleanAttributeName;
7587
}
7688

7789
/**
7890
* Creates a value token to be used with an ExpressionValues map.
7991
*/
8092
public static String valueRef(String value) {
81-
return ":AMZN_MAPPED_" + cleanAttributeName(value);
93+
String cleanAttributeName = cleanAttributeName(value);
94+
cleanAttributeName = isNestedAttribute(cleanAttributeName) ?
95+
NESTED_OBJECT_PATTERN.matcher(cleanAttributeName).replaceAll("_")
96+
: cleanAttributeName;
97+
return ":AMZN_MAPPED_" + cleanAttributeName;
8298
}
8399

84100
public static <T> T readAndTransformSingleItem(Map<String, AttributeValue> itemMap,

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb.internal.operations;
1717

18+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
1819
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.readAndTransformSingleItem;
1920
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.operationExpression;
2021
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;
2122

2223
import java.util.Collection;
24+
import java.util.HashMap;
2325
import java.util.List;
2426
import java.util.Map;
2527
import java.util.Optional;
@@ -53,7 +55,8 @@
5355
public class UpdateItemOperation<T>
5456
implements TableOperation<T, UpdateItemRequest, UpdateItemResponse, UpdateItemEnhancedResponse<T>>,
5557
TransactableWriteOperation<T> {
56-
58+
59+
public static final String NESTED_OBJECT_UPDATE = "_NESTED_ATTR_UPDATE_";
5760
private final Either<UpdateItemEnhancedRequest<T>, TransactUpdateItemEnhancedRequest<T>> request;
5861

5962
private UpdateItemOperation(UpdateItemEnhancedRequest<T> request) {
@@ -89,8 +92,14 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,
8992
Boolean ignoreNulls = request.map(r -> Optional.ofNullable(r.ignoreNulls()),
9093
r -> Optional.ofNullable(r.ignoreNulls()))
9194
.orElse(null);
92-
93-
Map<String, AttributeValue> itemMap = tableSchema.itemToMap(item, Boolean.TRUE.equals(ignoreNulls));
95+
96+
Map<String, AttributeValue> itemMapImmutable = tableSchema.itemToMap(item, Boolean.TRUE.equals(ignoreNulls));
97+
98+
// If ignoreNulls is set to true, check for nested params to be updated
99+
// If needed, Transform itemMap for it to be able to handle them.
100+
Map<String, AttributeValue> itemMap = Boolean.TRUE.equals(ignoreNulls) ?
101+
transformItemToMapForUpdateExpression(itemMapImmutable) : itemMapImmutable;
102+
94103
TableMetadata tableMetadata = tableSchema.tableMetadata();
95104

96105
WriteModification transformation =
@@ -141,6 +150,58 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,
141150

142151
return requestBuilder.build();
143152
}
153+
154+
/**
155+
* Method checks if a nested object parameter requires an update
156+
* If so flattens out nested params separated by "_NESTED_ATTR_UPDATE_"
157+
* this is consumed by @link EnhancedClientUtils to form the appropriate UpdateExpression
158+
*/
159+
public Map<String, AttributeValue> transformItemToMapForUpdateExpression(Map<String, AttributeValue> itemToMap) {
160+
161+
Map<String, AttributeValue> nestedAttributes = new HashMap<>();
162+
163+
itemToMap.forEach((key, value) -> {
164+
if (value.hasM() && isNotEmptyMap(value.m())) {
165+
nestedAttributes.put(key, value);
166+
}
167+
});
168+
169+
if (!nestedAttributes.isEmpty()) {
170+
Map<String, AttributeValue> itemToMapMutable = new HashMap<>(itemToMap);
171+
nestedAttributes.forEach((key, value) -> {
172+
itemToMapMutable.remove(key);
173+
nestedItemToMap(itemToMapMutable, key, value);
174+
});
175+
return itemToMapMutable;
176+
}
177+
178+
return itemToMap;
179+
}
180+
181+
private Map<String, AttributeValue> nestedItemToMap(Map<String, AttributeValue> itemToMap,
182+
String key,
183+
AttributeValue attributeValue) {
184+
attributeValue.m().forEach((mapKey, mapValue) -> {
185+
String nestedAttributeKey = key + NESTED_OBJECT_UPDATE + mapKey;
186+
if (attributeValueNonNullOrShouldWriteNull(mapValue)) {
187+
if (mapValue.hasM()) {
188+
nestedItemToMap(itemToMap, nestedAttributeKey, mapValue);
189+
} else {
190+
itemToMap.put(nestedAttributeKey, mapValue);
191+
}
192+
}
193+
});
194+
return itemToMap;
195+
}
196+
197+
private boolean isNotEmptyMap(Map<String, AttributeValue> map) {
198+
return !map.isEmpty() && map.values().stream()
199+
.anyMatch(this::attributeValueNonNullOrShouldWriteNull);
200+
}
201+
202+
private boolean attributeValueNonNullOrShouldWriteNull(AttributeValue attributeValue) {
203+
return !isNullAttributeValue(attributeValue);
204+
}
144205

145206
@Override
146207
public UpdateItemEnhancedResponse<T> transformResponse(UpdateItemResponse response,

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/update/UpdateExpressionUtils.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
1919
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef;
2020
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef;
21+
import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE;
2122
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;
2223

2324
import java.util.Arrays;
2425
import java.util.Collections;
2526
import java.util.List;
2627
import java.util.Map;
2728
import java.util.function.Function;
29+
import java.util.regex.Pattern;
2830
import java.util.stream.Collectors;
2931
import software.amazon.awssdk.annotations.SdkInternalApi;
3032
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
@@ -39,6 +41,8 @@
3941
@SdkInternalApi
4042
public final class UpdateExpressionUtils {
4143

44+
private static final Pattern PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE);
45+
4246
private UpdateExpressionUtils() {
4347
}
4448

@@ -136,9 +140,12 @@ private static Function<String, String> behaviorBasedValue(UpdateBehavior update
136140
/**
137141
* Simple utility method that can create an ExpressionNames map based on a list of attribute names.
138142
*/
139-
private static Map<String, String> expressionNamesFor(String... attributeNames) {
140-
return Arrays.stream(attributeNames)
141-
.collect(Collectors.toMap(EnhancedClientUtils::keyRef, Function.identity()));
142-
}
143+
private static Map<String, String> expressionNamesFor(String attributeNames) {
144+
if (attributeNames.contains(NESTED_OBJECT_UPDATE)) {
145+
return Arrays.stream(PATTERN.split(attributeNames)).distinct()
146+
.collect(Collectors.toMap(EnhancedClientUtils::keyRef, Function.identity()));
147+
}
143148

149+
return Collections.singletonMap(keyRef(attributeNames), attributeNames);
150+
}
144151
}

0 commit comments

Comments
 (0)