Skip to content

Commit 84679ed

Browse files
committed
DynamoDB Enhanced Client: Added support for attribute level custom update behaviors
1 parent 35267ca commit 84679ed

File tree

11 files changed

+411
-10
lines changed

11 files changed

+411
-10
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS DynamoDB Enhanced Client",
4+
"description": "Added support for attribute level custom update behaviors such as 'write if not exists'."
5+
}

services-custom/dynamodb-enhanced/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,47 @@ private static final StaticTableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
432432
.build();
433433
```
434434

435+
### Changing update behavior of attributes
436+
It is possible to customize the update behavior as applicable to individual attributes when an 'update' operation is
437+
performed (e.g. UpdateItem or an update within TransactWriteItems).
438+
439+
For example, say like you wanted to store a 'created on' timestamp on your record, but only wanted its value to be
440+
written if there is no existing value for the attribute stored in the database then you would use the
441+
WRITE_IF_NOT_EXISTS update behavior. Here is an example using a bean:
442+
443+
```java
444+
@DynamoDbBean
445+
public class Customer extends GenericRecord {
446+
private String id;
447+
private Instant createdOn;
448+
449+
@DynamoDbPartitionKey
450+
public String getId() { return this.id; }
451+
public void setId(String id) { this.name = id; }
452+
453+
@DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)
454+
public Instant getCreatedOn() { return this.createdOn; }
455+
public void setCreatedOn(Instant createdOn) { this.createdOn = createdOn; }
456+
}
457+
```
458+
459+
Same example using a static table schema:
460+
461+
```java
462+
static final TableSchema<Customer> CUSTOMER_TABLE_SCHEMA =
463+
TableSchema.builder(Customer.class)
464+
.newItemSupplier(Customer::new)
465+
.addAttribute(String.class, a -> a.name("id")
466+
.getter(Customer::getId)
467+
.setter(Customer::setId)
468+
.tags(primaryPartitionKey()))
469+
.addAttribute(Instant.class, a -> a.name("createdOn")
470+
.getter(Customer::getCreatedOn)
471+
.setter(Customer::setCreatedOn)
472+
.tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS)))
473+
.build();
474+
```
475+
435476
### Flat map attributes from another class
436477
If the attributes for your table record are spread across several
437478
different Java objects, either through inheritance or composition, the

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey;
2626
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey;
2727
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
28+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
2829

2930
/**
3031
* Static provider class for core {@link BeanTableSchema} attribute tags. Each of the implemented annotations has a
@@ -52,4 +53,8 @@ public static StaticAttributeTag attributeTagFor(DynamoDbSecondaryPartitionKey a
5253
public static StaticAttributeTag attributeTagFor(DynamoDbSecondarySortKey annotation) {
5354
return StaticAttributeTags.secondarySortKey(Arrays.asList(annotation.indexNames()));
5455
}
56+
57+
public static StaticAttributeTag attributeTagFor(DynamoDbUpdateBehavior annotation) {
58+
return StaticAttributeTags.updateBehavior(annotation.value());
59+
}
5560
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.mapper;
17+
18+
import java.util.function.Consumer;
19+
import software.amazon.awssdk.annotations.SdkInternalApi;
20+
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
21+
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
22+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
23+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
24+
import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
25+
26+
@SdkInternalApi
27+
public class UpdateBehaviorTag implements StaticAttributeTag {
28+
private static final String CUSTOM_METADATA_KEY_PREFIX = "UpdateBehavior:";
29+
private static final UpdateBehavior DEFAULT_UPDATE_BEHAVIOR = UpdateBehavior.WRITE_ALWAYS;
30+
private static final UpdateBehaviorTag WRITE_ALWAYS_TAG = new UpdateBehaviorTag(UpdateBehavior.WRITE_ALWAYS);
31+
private static final UpdateBehaviorTag WRITE_IF_NOT_EXISTS_TAG =
32+
new UpdateBehaviorTag(UpdateBehavior.WRITE_IF_NOT_EXISTS);
33+
34+
private final UpdateBehavior updateBehavior;
35+
36+
private UpdateBehaviorTag(UpdateBehavior updateBehavior) {
37+
this.updateBehavior = updateBehavior;
38+
}
39+
40+
public static UpdateBehaviorTag fromUpdateBehavior(UpdateBehavior updateBehavior) {
41+
switch (updateBehavior) {
42+
case WRITE_ALWAYS:
43+
return WRITE_ALWAYS_TAG;
44+
case WRITE_IF_NOT_EXISTS:
45+
return WRITE_IF_NOT_EXISTS_TAG;
46+
default:
47+
throw new IllegalArgumentException("Update behavior '" + updateBehavior + "' not supported");
48+
}
49+
}
50+
51+
public static UpdateBehavior resolveForAttribute(String attributeName, TableMetadata tableMetadata) {
52+
String metadataKey = CUSTOM_METADATA_KEY_PREFIX + attributeName;
53+
return tableMetadata.customMetadataObject(metadataKey, UpdateBehavior.class).orElse(DEFAULT_UPDATE_BEHAVIOR);
54+
}
55+
56+
@Override
57+
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName,
58+
AttributeValueType attributeValueType) {
59+
return metadata ->
60+
metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY_PREFIX + attributeName, this.updateBehavior);
61+
}
62+
}

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification;
3636
import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils;
3737
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext;
38+
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag;
39+
import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
3840
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3941
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
4042
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -56,6 +58,9 @@ public class UpdateItemOperation<T>
5658
private static final Function<String, String> EXPRESSION_KEY_MAPPER =
5759
key -> "#AMZN_MAPPED_" + EnhancedClientUtils.cleanAttributeName(key);
5860

61+
private static final Function<String, String> CONDITIONAL_UPDATE_MAPPER =
62+
key -> "if_not_exists(" + key + ", " + EXPRESSION_VALUE_KEY_MAPPER.apply(key) + ")";
63+
5964
private final UpdateItemEnhancedRequest<T> request;
6065

6166
private UpdateItemOperation(UpdateItemEnhancedRequest<T> request) {
@@ -104,7 +109,7 @@ public UpdateItemRequest generateRequest(TableSchema<T> tableSchema,
104109
.filter(entry -> !primaryKeys.contains(entry.getKey()))
105110
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
106111

107-
requestBuilder = addExpressionsIfExist(transformation, filteredAttributeValues, requestBuilder);
112+
requestBuilder = addExpressionsIfExist(transformation, filteredAttributeValues, requestBuilder, tableMetadata);
108113

109114
return requestBuilder.build();
110115
}
@@ -157,14 +162,17 @@ public TransactWriteItem generateTransactWriteItem(TableSchema<T> tableSchema, O
157162
.build();
158163
}
159164

160-
private static Expression generateUpdateExpression(Map<String, AttributeValue> attributeValuesToUpdate) {
165+
private static Expression generateUpdateExpression(Map<String, AttributeValue> attributeValuesToUpdate,
166+
TableMetadata tableMetadata) {
161167
// Sort the updates into 'SET' or 'REMOVE' based on null value
162168
List<String> updateSetActions = new ArrayList<>();
163169
List<String> updateRemoveActions = new ArrayList<>();
164170

165171
attributeValuesToUpdate.forEach((key, value) -> {
166172
if (!isNullAttributeValue(value)) {
167-
updateSetActions.add(EXPRESSION_KEY_MAPPER.apply(key) + " = " + EXPRESSION_VALUE_KEY_MAPPER.apply(key));
173+
UpdateBehavior updateBehavior = UpdateBehaviorTag.resolveForAttribute(key, tableMetadata);
174+
updateSetActions.add(EXPRESSION_KEY_MAPPER.apply(key) + " = " +
175+
updateExpressionMapperForBehavior(updateBehavior).apply(key));
168176
} else {
169177
updateRemoveActions.add(EXPRESSION_KEY_MAPPER.apply(key));
170178
}
@@ -203,16 +211,28 @@ private static Expression generateUpdateExpression(Map<String, AttributeValue> a
203211
.build();
204212
}
205213

214+
private static Function<String, String> updateExpressionMapperForBehavior(UpdateBehavior updateBehavior) {
215+
switch (updateBehavior) {
216+
case WRITE_ALWAYS:
217+
return EXPRESSION_VALUE_KEY_MAPPER;
218+
case WRITE_IF_NOT_EXISTS:
219+
return CONDITIONAL_UPDATE_MAPPER;
220+
default:
221+
throw new IllegalArgumentException("Unsupported update behavior '" + updateBehavior + "'");
222+
}
223+
}
224+
206225
private UpdateItemRequest.Builder addExpressionsIfExist(WriteModification transformation,
207226
Map<String, AttributeValue> filteredAttributeValues,
208-
UpdateItemRequest.Builder requestBuilder) {
227+
UpdateItemRequest.Builder requestBuilder,
228+
TableMetadata tableMetadata) {
209229
Map<String, String> expressionNames = null;
210230
Map<String, AttributeValue> expressionValues = null;
211231
String conditionExpressionString = null;
212232

213233
/* Add update expression for transformed non-key attributes if applicable */
214234
if (!filteredAttributeValues.isEmpty()) {
215-
Expression fullUpdateExpression = generateUpdateExpression(filteredAttributeValues);
235+
Expression fullUpdateExpression = generateUpdateExpression(filteredAttributeValues, tableMetadata);
216236
expressionNames = fullUpdateExpression.expressionNames();
217237
expressionValues = fullUpdateExpression.expressionValues();
218238
requestBuilder = requestBuilder.updateExpression(fullUpdateExpression.expression());

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import software.amazon.awssdk.annotations.SdkPublicApi;
2222
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
2323
import software.amazon.awssdk.enhanced.dynamodb.TableMetadata;
24+
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag;
2425

2526
/**
2627
* Common implementations of {@link StaticAttributeTag}. These tags can be used to mark your attributes as primary or
@@ -106,6 +107,16 @@ public static StaticAttributeTag secondarySortKey(Collection<String> indexNames)
106107
attribute.getAttributeValueType())));
107108
}
108109

110+
/**
111+
* Specifies the behavior when this attribute is updated as part of an 'update' operation such as UpdateItem. See
112+
* documentation of {@link UpdateBehavior} for details on the different behaviors supported and the default
113+
* behavior.
114+
* @param updateBehavior The {@link UpdateBehavior} to be applied to this attribute
115+
*/
116+
public static StaticAttributeTag updateBehavior(UpdateBehavior updateBehavior) {
117+
return UpdateBehaviorTag.fromUpdateBehavior(updateBehavior);
118+
}
119+
109120
private static class KeyAttributeTag implements StaticAttributeTag {
110121
private final BiConsumer<StaticTableMetadata.Builder, AttributeAndType> tableMetadataKeySetter;
111122

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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;
17+
18+
import software.amazon.awssdk.annotations.SdkPublicApi;
19+
20+
/**
21+
* Update behaviors that can be applied to individual attributes. This behavior will only apply to 'update' operations
22+
* such as UpdateItem, and not 'put' operations such as PutItem.
23+
* <p>
24+
* If an update behavior is not specified for an attribute, the default behavior of {@link #WRITE_ALWAYS} will be
25+
* applied.
26+
*/
27+
@SdkPublicApi
28+
public enum UpdateBehavior {
29+
/**
30+
* Always overwrite with the new value if one is provided, or remove any existing value if a null value is
31+
* provided and 'ignoreNulls' is set to false.
32+
* <p>
33+
* This is the default behavior applied to all attributes unless otherwise specified.
34+
*/
35+
WRITE_ALWAYS,
36+
37+
/**
38+
* Write the new value if there is no existing value in the persisted record or a new record is being written,
39+
* otherwise leave the existing value.
40+
* <p>
41+
* IMPORTANT: If a null value is provided and 'ignoreNulls' is set to false, the attribute
42+
* will always be removed from the persisted record as DynamoDb does not support conditional removal with this
43+
* method.
44+
*/
45+
WRITE_IF_NOT_EXISTS
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.annotations;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
import software.amazon.awssdk.annotations.SdkPublicApi;
23+
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags;
24+
import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior;
25+
26+
/**
27+
* Specifies the behavior when this attribute is updated as part of an 'update' operation such as UpdateItem. See
28+
* documentation of {@link UpdateBehavior} for details on the different behaviors supported and the default behavior.
29+
*/
30+
@SdkPublicApi
31+
@Target({ElementType.METHOD})
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@BeanTableSchemaAttributeTag(BeanTableSchemaAttributeTags.class)
34+
public @interface DynamoDbUpdateBehavior {
35+
UpdateBehavior value();
36+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
2020
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
2121
import software.amazon.awssdk.enhanced.dynamodb.Expression;
22-
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
23-
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
2422

2523
/**
2624
* Defines parameters used to update an item to a DynamoDb table using the updateItem() operation (such as
@@ -123,9 +121,10 @@ private Builder() {
123121
/**
124122
* Sets if the update operation should ignore attributes with null values. By default, the value is false.
125123
* <p>
126-
* If set to true, any null values in the Java object will not be written to the table.
127-
* If set to false, null values in the Java object will be written to the table (as an {@link AttributeValue} of type
128-
* 'nul' in the output map, see {@link TableSchema#itemToMap(Object, boolean)}).
124+
* If set to true, any null values in the Java object will be ignored and not be updated on the persisted
125+
* record. This is commonly referred to as a 'partial update'.
126+
* If set to false, null values in the Java object will cause those attributes to be removed from the persisted
127+
* record on update.
129128
* @param ignoreNulls the boolean value
130129
* @return a builder of this type
131130
*/

0 commit comments

Comments
 (0)