Skip to content

Commit f222216

Browse files
committed
DynamoDb enhanced client: support UpdateExpressions in single-request update
1 parent fd6b4a2 commit f222216

File tree

10 files changed

+422
-104
lines changed

10 files changed

+422
-104
lines changed

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

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
1919
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.readAndTransformSingleItem;
20-
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.operationExpression;
2120
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;
2221

2322
import java.util.Collection;
@@ -36,6 +35,7 @@
3635
import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification;
3736
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
3837
import software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionConverter;
38+
import software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionResolver;
3939
import software.amazon.awssdk.enhanced.dynamodb.model.IgnoreNullsMode;
4040
import software.amazon.awssdk.enhanced.dynamodb.model.TransactUpdateItemEnhancedRequest;
4141
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
@@ -132,7 +132,7 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,
132132
Map<String, AttributeValue> keyAttributes = filterMap(itemMap, entry -> primaryKeys.contains(entry.getKey()));
133133
Map<String, AttributeValue> nonKeyAttributes = filterMap(itemMap, entry -> !primaryKeys.contains(entry.getKey()));
134134

135-
Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, nonKeyAttributes);
135+
Expression updateExpression = generateUpdateExpressionIfExist(tableMetadata, transformation, request, nonKeyAttributes);
136136
Expression conditionExpression = generateConditionExpressionIfExist(transformation, request);
137137

138138
Map<String, String> expressionNames = coalesceExpressionNames(updateExpression, conditionExpression);
@@ -275,23 +275,26 @@ public TransactWriteItem generateTransactWriteItem(TableSchema<T> tableSchema, O
275275
* if there are attributes to be updated (most likely). If both exist, they are merged and the code generates a final
276276
* Expression that represent the result.
277277
*/
278-
private Expression generateUpdateExpressionIfExist(TableMetadata tableMetadata,
279-
WriteModification transformation,
280-
Map<String, AttributeValue> attributes) {
281-
UpdateExpression updateExpression = null;
282-
if (transformation != null && transformation.updateExpression() != null) {
283-
updateExpression = transformation.updateExpression();
284-
}
285-
if (!attributes.isEmpty()) {
286-
List<String> nonRemoveAttributes = UpdateExpressionConverter.findAttributeNames(updateExpression);
287-
UpdateExpression operationUpdateExpression = operationExpression(attributes, tableMetadata, nonRemoveAttributes);
288-
if (updateExpression == null) {
289-
updateExpression = operationUpdateExpression;
290-
} else {
291-
updateExpression = UpdateExpression.mergeExpressions(updateExpression, operationUpdateExpression);
292-
}
293-
}
294-
return UpdateExpressionConverter.toExpression(updateExpression);
278+
private Expression generateUpdateExpressionIfExist(
279+
TableMetadata tableMetadata,
280+
WriteModification transformation,
281+
Either<UpdateItemEnhancedRequest<T>, TransactUpdateItemEnhancedRequest<T>> request,
282+
Map<String, AttributeValue> nonKeyAttributes) {
283+
284+
UpdateExpression requestUpdateExpression = request.map(r -> Optional.ofNullable(r.updateExpression()),
285+
r -> Optional.ofNullable(r.updateExpression()))
286+
.orElse(null);
287+
288+
UpdateExpressionResolver updateExpressionResolver =
289+
UpdateExpressionResolver.builder()
290+
.tableMetadata(tableMetadata)
291+
.itemNonKeyAttributes(nonKeyAttributes)
292+
.requestExpression(requestUpdateExpression)
293+
.transformationExpression(transformation != null ? transformation.updateExpression() : null)
294+
.build();
295+
296+
UpdateExpression mergedUpdateExpression = updateExpressionResolver.resolve();
297+
return UpdateExpressionConverter.toExpression(mergedUpdateExpression);
295298
}
296299

297300
/**

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ private UpdateExpressionConverter() {
7676
* @return an Expression representing the concatenation of all actions in this UpdateExpression
7777
*/
7878
public static Expression toExpression(UpdateExpression expression) {
79-
if (expression == null) {
79+
if (expression == null || expression.isEmpty()) {
8080
return null;
8181
}
8282
Map<String, AttributeValue> expressionValues = mergeExpressionValues(expression);
@@ -91,8 +91,8 @@ public static Expression toExpression(UpdateExpression expression) {
9191
}
9292

9393
/**
94-
* Attempts to find the list of attribute names that will be updated for the supplied {@link UpdateExpression} by looking at
95-
* the combined collection of paths and ExpressionName values. Because attribute names can be composed from nested
94+
* Attempts to find the list of attributes associated with update actions for the supplied {@link UpdateExpression} by
95+
* looking at the combined collection of paths and ExpressionName values. Because attribute names can be composed of nested
9696
* attribute references and list references, the leftmost part will be returned if composition is detected.
9797
* <p>
9898
* Examples: The expression contains a {@link DeleteAction} with a path value of 'MyAttribute[1]'; the list returned
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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.internal.update;
17+
18+
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
19+
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.removeActionsFor;
20+
import static software.amazon.awssdk.enhanced.dynamodb.internal.update.UpdateExpressionUtils.setActionsFor;
21+
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;
22+
23+
import java.util.Arrays;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.stream.Collectors;
27+
import software.amazon.awssdk.annotations.SdkInternalApi;
28+
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
29+
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
30+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
31+
32+
/**
33+
*
34+
*/
35+
@SdkInternalApi
36+
public final class UpdateExpressionResolver {
37+
38+
private final UpdateExpression extensionExpression;
39+
private final UpdateExpression requestExpression;
40+
private final Map<String, AttributeValue> itemNonKeyAttributes;
41+
private final TableMetadata tableMetadata;
42+
43+
private UpdateExpressionResolver(Builder builder) {
44+
this.extensionExpression = builder.transformationExpression;
45+
this.requestExpression = builder.requestExpression;
46+
this.itemNonKeyAttributes = builder.nonKeyAttributes;
47+
this.tableMetadata = builder.tableMetadata;
48+
}
49+
50+
public static Builder builder() {
51+
return new Builder();
52+
}
53+
54+
/**
55+
* Resolves all available and potential update expressions by priority and returns a merged update expression. It may return
56+
* null, if the item attribute map is empty / does not contain non-null attributes and no other update expressions are
57+
* present.
58+
* <p>
59+
* Conditions that will result in error:
60+
* <ul>
61+
* <li>Two expressions contain actions referencing the same attribute</li>
62+
* </ul>
63+
* <p>
64+
* <b>Note: </b> The presence of attributes in update expressions submitted through the request or generated from extensions
65+
* take precedence over removing attributes based on item configuration.
66+
* For example, when IGNORE_NULLS is set to true (default), the client generates REMOVE actions for all
67+
* attributes in the schema that are not explicitly set in the request item submitted to the operation. If such
68+
* attributes are referenced in update expressions on the request or from extensions, the remove actions are filtered
69+
* out.
70+
*/
71+
public UpdateExpression resolve() {
72+
UpdateExpression itemSetExpression = generateItemSetExpression(itemNonKeyAttributes, tableMetadata);
73+
74+
List<String> nonRemoveAttributes = attributesPresentInExpressions(Arrays.asList(extensionExpression, requestExpression));
75+
UpdateExpression itemRemoveExpression = generateItemRemoveExpression(itemNonKeyAttributes, nonRemoveAttributes);
76+
77+
UpdateExpression itemExpression = UpdateExpression.mergeExpressions(itemSetExpression, itemRemoveExpression);
78+
UpdateExpression extensionItemExpression = UpdateExpression.mergeExpressions(extensionExpression, itemExpression);
79+
return UpdateExpression.mergeExpressions(requestExpression, extensionItemExpression);
80+
}
81+
82+
private static List<String> attributesPresentInExpressions(List<UpdateExpression> updateExpressions) {
83+
return updateExpressions.stream()
84+
.map(UpdateExpressionConverter::findAttributeNames)
85+
.flatMap(List::stream)
86+
.collect(Collectors.toList());
87+
}
88+
89+
public static UpdateExpression generateItemSetExpression(Map<String, AttributeValue> itemMap,
90+
TableMetadata tableMetadata) {
91+
92+
Map<String, AttributeValue> setAttributes = filterMap(itemMap, e -> !isNullAttributeValue(e.getValue()));
93+
return UpdateExpression.builder()
94+
.actions(setActionsFor(setAttributes, tableMetadata))
95+
.build();
96+
}
97+
98+
public static UpdateExpression generateItemRemoveExpression(Map<String, AttributeValue> itemMap,
99+
List<String> nonRemoveAttributes) {
100+
Map<String, AttributeValue> removeAttributes =
101+
filterMap(itemMap, e -> isNullAttributeValue(e.getValue()) && !nonRemoveAttributes.contains(e.getKey()));
102+
103+
return UpdateExpression.builder()
104+
.actions(removeActionsFor(removeAttributes))
105+
.build();
106+
}
107+
108+
public static final class Builder {
109+
110+
private TableMetadata tableMetadata;
111+
private UpdateExpression transformationExpression;
112+
private UpdateExpression requestExpression;
113+
private Map<String, AttributeValue> nonKeyAttributes;
114+
115+
public Builder tableMetadata(TableMetadata tableMetadata) {
116+
this.tableMetadata = tableMetadata;
117+
return this;
118+
}
119+
120+
public Builder transformationExpression(UpdateExpression transformationExpression) {
121+
this.transformationExpression = transformationExpression;
122+
return this;
123+
}
124+
125+
public Builder itemNonKeyAttributes(Map<String, AttributeValue> nonKeyAttributes) {
126+
this.nonKeyAttributes = nonKeyAttributes;
127+
return this;
128+
}
129+
130+
public Builder requestExpression(UpdateExpression requestExpression) {
131+
this.requestExpression = requestExpression;
132+
return this;
133+
}
134+
135+
public UpdateExpressionResolver build() {
136+
return new UpdateExpressionResolver(this);
137+
}
138+
139+
}
140+
}

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

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@
1515

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

18-
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue;
1918
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.keyRef;
2019
import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.valueRef;
2120
import static software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation.NESTED_OBJECT_UPDATE;
22-
import static software.amazon.awssdk.utils.CollectionUtils.filterMap;
2321

2422
import java.util.Arrays;
2523
import java.util.Collections;
@@ -35,7 +33,6 @@
3533
import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
3634
import software.amazon.awssdk.enhanced.dynamodb.update.RemoveAction;
3735
import software.amazon.awssdk.enhanced.dynamodb.update.SetAction;
38-
import software.amazon.awssdk.enhanced.dynamodb.update.UpdateExpression;
3936
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
4037

4138
@SdkInternalApi
@@ -53,32 +50,10 @@ public static String ifNotExists(String key, String initValue) {
5350
return "if_not_exists(" + keyRef(key) + ", " + valueRef(initValue) + ")";
5451
}
5552

56-
/**
57-
* Generates an UpdateExpression representing a POJO, with only SET and REMOVE actions.
58-
*/
59-
public static UpdateExpression operationExpression(Map<String, AttributeValue> itemMap,
60-
TableMetadata tableMetadata,
61-
List<String> nonRemoveAttributes) {
62-
63-
Map<String, AttributeValue> setAttributes = filterMap(itemMap, e -> !isNullAttributeValue(e.getValue()));
64-
UpdateExpression setAttributeExpression = UpdateExpression.builder()
65-
.actions(setActionsFor(setAttributes, tableMetadata))
66-
.build();
67-
68-
Map<String, AttributeValue> removeAttributes =
69-
filterMap(itemMap, e -> isNullAttributeValue(e.getValue()) && !nonRemoveAttributes.contains(e.getKey()));
70-
71-
UpdateExpression removeAttributeExpression = UpdateExpression.builder()
72-
.actions(removeActionsFor(removeAttributes))
73-
.build();
74-
75-
return UpdateExpression.mergeExpressions(setAttributeExpression, removeAttributeExpression);
76-
}
77-
7853
/**
7954
* Creates a list of SET actions for all attributes supplied in the map.
8055
*/
81-
private static List<SetAction> setActionsFor(Map<String, AttributeValue> attributesToSet, TableMetadata tableMetadata) {
56+
public static List<SetAction> setActionsFor(Map<String, AttributeValue> attributesToSet, TableMetadata tableMetadata) {
8257
return attributesToSet.entrySet()
8358
.stream()
8459
.map(entry -> setValue(entry.getKey(),
@@ -90,7 +65,7 @@ private static List<SetAction> setActionsFor(Map<String, AttributeValue> attribu
9065
/**
9166
* Creates a list of REMOVE actions for all attributes supplied in the map.
9267
*/
93-
private static List<RemoveAction> removeActionsFor(Map<String, AttributeValue> attributesToSet) {
68+
public static List<RemoveAction> removeActionsFor(Map<String, AttributeValue> attributesToSet) {
9469
return attributesToSet.entrySet()
9570
.stream()
9671
.map(entry -> remove(entry.getKey()))

0 commit comments

Comments
 (0)