Skip to content

Commit

Permalink
Fixed EnhancedClient UpdateItem operation to make it work on nested a…
Browse files Browse the repository at this point in the history
…ttributes as well (aws#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
  • Loading branch information
anirudh9391 authored Sep 4, 2024
1 parent 017292e commit 79394aa
Show file tree
Hide file tree
Showing 9 changed files with 486 additions and 9 deletions.
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-2a80679.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "AWS SDK for Java v2",
"contributor": "anirudh9391",
"description": "Introduce a new method to transform input to be able to perform update operations on nested DynamoDB object attributes."
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@

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

import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -40,6 +43,7 @@ public final class EnhancedClientUtils {
private static final Set<Character> SPECIAL_CHARACTERS = Stream.of(
'*', '.', '-', '#', '+', ':', '/', '(', ')', ' ',
'&', '<', '>', '?', '=', '!', '@', '%', '$', '|').collect(Collectors.toSet());
private static final Pattern NESTED_OBJECT_PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE);

private EnhancedClientUtils() {

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

private static boolean isNestedAttribute(String key) {
return key.contains(NESTED_OBJECT_UPDATE);
}

/**
* Creates a key token to be used with an ExpressionNames map.
*/
public static String keyRef(String key) {
return "#AMZN_MAPPED_" + cleanAttributeName(key);
String cleanAttributeName = cleanAttributeName(key);
cleanAttributeName = isNestedAttribute(cleanAttributeName) ?
NESTED_OBJECT_PATTERN.matcher(cleanAttributeName).replaceAll(".#AMZN_MAPPED_")
: cleanAttributeName;
return "#AMZN_MAPPED_" + cleanAttributeName;
}

/**
* Creates a value token to be used with an ExpressionValues map.
*/
public static String valueRef(String value) {
return ":AMZN_MAPPED_" + cleanAttributeName(value);
String cleanAttributeName = cleanAttributeName(value);
cleanAttributeName = isNestedAttribute(cleanAttributeName) ?
NESTED_OBJECT_PATTERN.matcher(cleanAttributeName).replaceAll("_")
: cleanAttributeName;
return ":AMZN_MAPPED_" + cleanAttributeName;
}

public static <T> T readAndTransformSingleItem(Map<String, AttributeValue> itemMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@

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

import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.readAndTransformSingleItem;
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.operationExpression;
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -53,7 +55,8 @@
public class UpdateItemOperation<T>
implements TableOperation<T, UpdateItemRequest, UpdateItemResponse, UpdateItemEnhancedResponse<T>>,
TransactableWriteOperation<T> {


public static final String NESTED_OBJECT_UPDATE = "_NESTED_ATTR_UPDATE_";
private final Either<UpdateItemEnhancedRequest<T>, TransactUpdateItemEnhancedRequest<T>> request;

private UpdateItemOperation(UpdateItemEnhancedRequest<T> request) {
Expand Down Expand Up @@ -89,8 +92,14 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,
Boolean ignoreNulls = request.map(r -> Optional.ofNullable(r.ignoreNulls()),
r -> Optional.ofNullable(r.ignoreNulls()))
.orElse(null);

Map<String, AttributeValue> itemMap = tableSchema.itemToMap(item, Boolean.TRUE.equals(ignoreNulls));

Map<String, AttributeValue> itemMapImmutable = tableSchema.itemToMap(item, Boolean.TRUE.equals(ignoreNulls));

// If ignoreNulls is set to true, check for nested params to be updated
// If needed, Transform itemMap for it to be able to handle them.
Map<String, AttributeValue> itemMap = Boolean.TRUE.equals(ignoreNulls) ?
transformItemToMapForUpdateExpression(itemMapImmutable) : itemMapImmutable;

TableMetadata tableMetadata = tableSchema.tableMetadata();

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

return requestBuilder.build();
}

/**
* Method checks if a nested object parameter requires an update
* If so flattens out nested params separated by "_NESTED_ATTR_UPDATE_"
* this is consumed by @link EnhancedClientUtils to form the appropriate UpdateExpression
*/
public Map<String, AttributeValue> transformItemToMapForUpdateExpression(Map<String, AttributeValue> itemToMap) {

Map<String, AttributeValue> nestedAttributes = new HashMap<>();

itemToMap.forEach((key, value) -> {
if (value.hasM() && isNotEmptyMap(value.m())) {
nestedAttributes.put(key, value);
}
});

if (!nestedAttributes.isEmpty()) {
Map<String, AttributeValue> itemToMapMutable = new HashMap<>(itemToMap);
nestedAttributes.forEach((key, value) -> {
itemToMapMutable.remove(key);
nestedItemToMap(itemToMapMutable, key, value);
});
return itemToMapMutable;
}

return itemToMap;
}

private Map<String, AttributeValue> nestedItemToMap(Map<String, AttributeValue> itemToMap,
String key,
AttributeValue attributeValue) {
attributeValue.m().forEach((mapKey, mapValue) -> {
String nestedAttributeKey = key + NESTED_OBJECT_UPDATE + mapKey;
if (attributeValueNonNullOrShouldWriteNull(mapValue)) {
if (mapValue.hasM()) {
nestedItemToMap(itemToMap, nestedAttributeKey, mapValue);
} else {
itemToMap.put(nestedAttributeKey, mapValue);
}
}
});
return itemToMap;
}

private boolean isNotEmptyMap(Map<String, AttributeValue> map) {
return !map.isEmpty() && map.values().stream()
.anyMatch(this::attributeValueNonNullOrShouldWriteNull);
}

private boolean attributeValueNonNullOrShouldWriteNull(AttributeValue attributeValue) {
return !isNullAttributeValue(attributeValue);
}

@Override
public UpdateItemEnhancedResponse<T> transformResponse(UpdateItemResponse response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef;
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef;
import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE;
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
Expand All @@ -39,6 +41,8 @@
@SdkInternalApi
public final class UpdateExpressionUtils {

private static final Pattern PATTERN = Pattern.compile(NESTED_OBJECT_UPDATE);

private UpdateExpressionUtils() {
}

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

return Collections.singletonMap(keyRef(attributeNames), attributeNames);
}
}
Loading

0 comments on commit 79394aa

Please sign in to comment.