From 78576238ea3469ddae74c57cf58fd6ce99701bed Mon Sep 17 00:00:00 2001 From: Ben Maizels Date: Wed, 30 Sep 2020 19:36:27 -0700 Subject: [PATCH] DynamoDB Enhanced Client: Added support for attribute level custom update behaviors --- ...ure-AWSDynamoDBEnhancedClient-52b325e.json | 5 + services-custom/dynamodb-enhanced/README.md | 41 +++++++ .../mapper/BeanTableSchemaAttributeTags.java | 5 + .../internal/mapper/UpdateBehaviorTag.java | 62 ++++++++++ .../operations/UpdateItemOperation.java | 30 ++++- .../dynamodb/mapper/StaticAttributeTags.java | 11 ++ .../dynamodb/mapper/UpdateBehavior.java | 46 ++++++++ .../annotations/DynamoDbUpdateBehavior.java | 36 ++++++ .../model/UpdateItemEnhancedRequest.java | 9 +- .../functionaltests/UpdateBehaviorTest.java | 107 ++++++++++++++++++ .../models/RecordWithUpdateBehaviors.java | 69 +++++++++++ 11 files changed, 411 insertions(+), 10 deletions(-) create mode 100644 .changes/next-release/feature-AWSDynamoDBEnhancedClient-52b325e.json create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/UpdateBehaviorTag.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/UpdateBehavior.java create mode 100644 services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java diff --git a/.changes/next-release/feature-AWSDynamoDBEnhancedClient-52b325e.json b/.changes/next-release/feature-AWSDynamoDBEnhancedClient-52b325e.json new file mode 100644 index 000000000000..e5508f54c68e --- /dev/null +++ b/.changes/next-release/feature-AWSDynamoDBEnhancedClient-52b325e.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "AWS DynamoDB Enhanced Client", + "description": "Added support for attribute level custom update behaviors such as 'write if not exists'." +} diff --git a/services-custom/dynamodb-enhanced/README.md b/services-custom/dynamodb-enhanced/README.md index b73b6b2be8dc..7c0bc378007e 100644 --- a/services-custom/dynamodb-enhanced/README.md +++ b/services-custom/dynamodb-enhanced/README.md @@ -432,6 +432,47 @@ private static final StaticTableSchema CUSTOMER_TABLE_SCHEMA = .build(); ``` +### Changing update behavior of attributes +It is possible to customize the update behavior as applicable to individual attributes when an 'update' operation is +performed (e.g. UpdateItem or an update within TransactWriteItems). + +For example, say like you wanted to store a 'created on' timestamp on your record, but only wanted its value to be +written if there is no existing value for the attribute stored in the database then you would use the +WRITE_IF_NOT_EXISTS update behavior. Here is an example using a bean: + +```java +@DynamoDbBean +public class Customer extends GenericRecord { + private String id; + private Instant createdOn; + + @DynamoDbPartitionKey + public String getId() { return this.id; } + public void setId(String id) { this.name = id; } + + @DynamoDbUpdateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS) + public Instant getCreatedOn() { return this.createdOn; } + public void setCreatedOn(Instant createdOn) { this.createdOn = createdOn; } +} +``` + +Same example using a static table schema: + +```java +static final TableSchema CUSTOMER_TABLE_SCHEMA = + TableSchema.builder(Customer.class) + .newItemSupplier(Customer::new) + .addAttribute(String.class, a -> a.name("id") + .getter(Customer::getId) + .setter(Customer::setId) + .tags(primaryPartitionKey())) + .addAttribute(Instant.class, a -> a.name("createdOn") + .getter(Customer::getCreatedOn) + .setter(Customer::setCreatedOn) + .tags(updateBehavior(UpdateBehavior.WRITE_IF_NOT_EXISTS))) + .build(); +``` + ### Flat map attributes from another class If the attributes for your table record are spread across several different Java objects, either through inheritance or composition, the diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java index 883529615a7a..0d19520badaf 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java @@ -25,6 +25,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; /** * 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 public static StaticAttributeTag attributeTagFor(DynamoDbSecondarySortKey annotation) { return StaticAttributeTags.secondarySortKey(Arrays.asList(annotation.indexNames())); } + + public static StaticAttributeTag attributeTagFor(DynamoDbUpdateBehavior annotation) { + return StaticAttributeTags.updateBehavior(annotation.value()); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/UpdateBehaviorTag.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/UpdateBehaviorTag.java new file mode 100644 index 000000000000..4b948a154d5c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/UpdateBehaviorTag.java @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.mapper; + +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; + +@SdkInternalApi +public class UpdateBehaviorTag implements StaticAttributeTag { + private static final String CUSTOM_METADATA_KEY_PREFIX = "UpdateBehavior:"; + private static final UpdateBehavior DEFAULT_UPDATE_BEHAVIOR = UpdateBehavior.WRITE_ALWAYS; + private static final UpdateBehaviorTag WRITE_ALWAYS_TAG = new UpdateBehaviorTag(UpdateBehavior.WRITE_ALWAYS); + private static final UpdateBehaviorTag WRITE_IF_NOT_EXISTS_TAG = + new UpdateBehaviorTag(UpdateBehavior.WRITE_IF_NOT_EXISTS); + + private final UpdateBehavior updateBehavior; + + private UpdateBehaviorTag(UpdateBehavior updateBehavior) { + this.updateBehavior = updateBehavior; + } + + public static UpdateBehaviorTag fromUpdateBehavior(UpdateBehavior updateBehavior) { + switch (updateBehavior) { + case WRITE_ALWAYS: + return WRITE_ALWAYS_TAG; + case WRITE_IF_NOT_EXISTS: + return WRITE_IF_NOT_EXISTS_TAG; + default: + throw new IllegalArgumentException("Update behavior '" + updateBehavior + "' not supported"); + } + } + + public static UpdateBehavior resolveForAttribute(String attributeName, TableMetadata tableMetadata) { + String metadataKey = CUSTOM_METADATA_KEY_PREFIX + attributeName; + return tableMetadata.customMetadataObject(metadataKey, UpdateBehavior.class).orElse(DEFAULT_UPDATE_BEHAVIOR); + } + + @Override + public Consumer modifyMetadata(String attributeName, + AttributeValueType attributeValueType) { + return metadata -> + metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY_PREFIX + attributeName, this.updateBehavior); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java index 489f7a4da493..3e181b78ae3e 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/UpdateItemOperation.java @@ -35,6 +35,8 @@ import software.amazon.awssdk.enhanced.dynamodb.extensions.WriteModification; import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils; import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.DefaultDynamoDbExtensionContext; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @@ -56,6 +58,9 @@ public class UpdateItemOperation private static final Function EXPRESSION_KEY_MAPPER = key -> "#AMZN_MAPPED_" + EnhancedClientUtils.cleanAttributeName(key); + private static final Function CONDITIONAL_UPDATE_MAPPER = + key -> "if_not_exists(" + key + ", " + EXPRESSION_VALUE_KEY_MAPPER.apply(key) + ")"; + private final UpdateItemEnhancedRequest request; private UpdateItemOperation(UpdateItemEnhancedRequest request) { @@ -104,7 +109,7 @@ public UpdateItemRequest generateRequest(TableSchema tableSchema, .filter(entry -> !primaryKeys.contains(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - requestBuilder = addExpressionsIfExist(transformation, filteredAttributeValues, requestBuilder); + requestBuilder = addExpressionsIfExist(transformation, filteredAttributeValues, requestBuilder, tableMetadata); return requestBuilder.build(); } @@ -157,14 +162,17 @@ public TransactWriteItem generateTransactWriteItem(TableSchema tableSchema, O .build(); } - private static Expression generateUpdateExpression(Map attributeValuesToUpdate) { + private static Expression generateUpdateExpression(Map attributeValuesToUpdate, + TableMetadata tableMetadata) { // Sort the updates into 'SET' or 'REMOVE' based on null value List updateSetActions = new ArrayList<>(); List updateRemoveActions = new ArrayList<>(); attributeValuesToUpdate.forEach((key, value) -> { if (!isNullAttributeValue(value)) { - updateSetActions.add(EXPRESSION_KEY_MAPPER.apply(key) + " = " + EXPRESSION_VALUE_KEY_MAPPER.apply(key)); + UpdateBehavior updateBehavior = UpdateBehaviorTag.resolveForAttribute(key, tableMetadata); + updateSetActions.add(EXPRESSION_KEY_MAPPER.apply(key) + " = " + + updateExpressionMapperForBehavior(updateBehavior).apply(key)); } else { updateRemoveActions.add(EXPRESSION_KEY_MAPPER.apply(key)); } @@ -203,16 +211,28 @@ private static Expression generateUpdateExpression(Map a .build(); } + private static Function updateExpressionMapperForBehavior(UpdateBehavior updateBehavior) { + switch (updateBehavior) { + case WRITE_ALWAYS: + return EXPRESSION_VALUE_KEY_MAPPER; + case WRITE_IF_NOT_EXISTS: + return CONDITIONAL_UPDATE_MAPPER; + default: + throw new IllegalArgumentException("Unsupported update behavior '" + updateBehavior + "'"); + } + } + private UpdateItemRequest.Builder addExpressionsIfExist(WriteModification transformation, Map filteredAttributeValues, - UpdateItemRequest.Builder requestBuilder) { + UpdateItemRequest.Builder requestBuilder, + TableMetadata tableMetadata) { Map expressionNames = null; Map expressionValues = null; String conditionExpressionString = null; /* Add update expression for transformed non-key attributes if applicable */ if (!filteredAttributeValues.isEmpty()) { - Expression fullUpdateExpression = generateUpdateExpression(filteredAttributeValues); + Expression fullUpdateExpression = generateUpdateExpression(filteredAttributeValues, tableMetadata); expressionNames = fullUpdateExpression.expressionNames(); expressionValues = fullUpdateExpression.expressionValues(); requestBuilder = requestBuilder.updateExpression(fullUpdateExpression.expression()); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java index e19645769bd8..6bd6255a4caf 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java @@ -21,6 +21,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.UpdateBehaviorTag; /** * 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 indexNames) attribute.getAttributeValueType()))); } + /** + * Specifies the behavior when this attribute is updated as part of an 'update' operation such as UpdateItem. See + * documentation of {@link UpdateBehavior} for details on the different behaviors supported and the default + * behavior. + * @param updateBehavior The {@link UpdateBehavior} to be applied to this attribute + */ + public static StaticAttributeTag updateBehavior(UpdateBehavior updateBehavior) { + return UpdateBehaviorTag.fromUpdateBehavior(updateBehavior); + } + private static class KeyAttributeTag implements StaticAttributeTag { private final BiConsumer tableMetadataKeySetter; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/UpdateBehavior.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/UpdateBehavior.java new file mode 100644 index 000000000000..f4d78deb7d49 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/UpdateBehavior.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Update behaviors that can be applied to individual attributes. This behavior will only apply to 'update' operations + * such as UpdateItem, and not 'put' operations such as PutItem. + *

+ * If an update behavior is not specified for an attribute, the default behavior of {@link #WRITE_ALWAYS} will be + * applied. + */ +@SdkPublicApi +public enum UpdateBehavior { + /** + * Always overwrite with the new value if one is provided, or remove any existing value if a null value is + * provided and 'ignoreNulls' is set to false. + *

+ * This is the default behavior applied to all attributes unless otherwise specified. + */ + WRITE_ALWAYS, + + /** + * Write the new value if there is no existing value in the persisted record or a new record is being written, + * otherwise leave the existing value. + *

+ * IMPORTANT: If a null value is provided and 'ignoreNulls' is set to false, the attribute + * will always be removed from the persisted record as DynamoDb does not support conditional removal with this + * method. + */ + WRITE_IF_NOT_EXISTS +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java new file mode 100644 index 000000000000..fa161446c1a4 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbUpdateBehavior.java @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.mapper.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior; + +/** + * Specifies the behavior when this attribute is updated as part of an 'update' operation such as UpdateItem. See + * documentation of {@link UpdateBehavior} for details on the different behaviors supported and the default behavior. + */ +@SdkPublicApi +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@BeanTableSchemaAttributeTag(BeanTableSchemaAttributeTags.class) +public @interface DynamoDbUpdateBehavior { + UpdateBehavior value(); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java index c375673bafe1..968425647220 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/UpdateItemEnhancedRequest.java @@ -19,8 +19,6 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Expression; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; /** * Defines parameters used to update an item to a DynamoDb table using the updateItem() operation (such as @@ -123,9 +121,10 @@ private Builder() { /** * Sets if the update operation should ignore attributes with null values. By default, the value is false. *

- * If set to true, any null values in the Java object will not be written to the table. - * If set to false, null values in the Java object will be written to the table (as an {@link AttributeValue} of type - * 'nul' in the output map, see {@link TableSchema#itemToMap(Object, boolean)}). + * If set to true, any null values in the Java object will be ignored and not be updated on the persisted + * record. This is commonly referred to as a 'partial update'. + * If set to false, null values in the Java object will cause those attributes to be removed from the persisted + * record on update. * @param ignoreNulls the boolean value * @return a builder of this type */ diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java new file mode 100644 index 000000000000..cef3796ed539 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java @@ -0,0 +1,107 @@ +package software.amazon.awssdk.enhanced.dynamodb.functionaltests; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.RecordWithUpdateBehaviors; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UpdateBehaviorTest extends LocalDynamoDbSyncTestBase { + private static final Instant INSTANT_1 = Instant.parse("2020-05-03T10:00:00Z"); + private static final Instant INSTANT_2 = Instant.parse("2020-05-03T10:05:00Z"); + + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(RecordWithUpdateBehaviors.class); + + private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(getDynamoDbClient()) + .build(); + + + private final DynamoDbTable mappedTable = + enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + @Before + public void createTable() { + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + } + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(r -> r.tableName(getConcreteTableName("table-name"))); + } + + @Test + public void updateBehaviors_firstUpdate() { + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); + record.setId("id123"); + record.setCreatedOn(INSTANT_1); + record.setLastUpdatedOn(INSTANT_2); + mappedTable.updateItem(record); + + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record); + assertThat(persistedRecord.getCreatedOn()).isEqualTo(INSTANT_1); + assertThat(persistedRecord.getLastUpdatedOn()).isEqualTo(INSTANT_2); + } + + @Test + public void updateBehaviors_secondUpdate() { + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); + record.setId("id123"); + record.setCreatedOn(INSTANT_1); + record.setLastUpdatedOn(INSTANT_2); + mappedTable.updateItem(record); + + record.setVersion(1L); + record.setCreatedOn(INSTANT_2); + record.setLastUpdatedOn(INSTANT_2); + mappedTable.updateItem(record); + + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record); + assertThat(persistedRecord.getCreatedOn()).isEqualTo(INSTANT_1); + assertThat(persistedRecord.getLastUpdatedOn()).isEqualTo(INSTANT_2); + } + + @Test + public void updateBehaviors_removal() { + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); + record.setId("id123"); + record.setCreatedOn(INSTANT_1); + record.setLastUpdatedOn(INSTANT_2); + mappedTable.updateItem(record); + + record.setVersion(1L); + record.setCreatedOn(null); + record.setLastUpdatedOn(null); + mappedTable.updateItem(record); + + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record); + assertThat(persistedRecord.getCreatedOn()).isNull(); + assertThat(persistedRecord.getLastUpdatedOn()).isNull(); + } + + @Test + public void updateBehaviors_transactWriteItems_secondUpdate() { + RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors(); + record.setId("id123"); + record.setCreatedOn(INSTANT_1); + record.setLastUpdatedOn(INSTANT_2); + mappedTable.updateItem(record); + + record.setVersion(1L); + record.setCreatedOn(INSTANT_2); + record.setLastUpdatedOn(INSTANT_2); + enhancedClient.transactWriteItems(r -> r.addUpdateItem(mappedTable, record)); + + RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record); + assertThat(persistedRecord.getCreatedOn()).isEqualTo(INSTANT_1); + assertThat(persistedRecord.getLastUpdatedOn()).isEqualTo(INSTANT_2); + } + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java new file mode 100644 index 000000000000..8dbcbdad829e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java @@ -0,0 +1,69 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import java.time.Instant; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior; + +import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_ALWAYS; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS; + +@DynamoDbBean +public class RecordWithUpdateBehaviors { + private String id; + private Instant createdOn; + private Instant lastUpdatedOn; + private Long version; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbUpdateBehavior(WRITE_IF_NOT_EXISTS) + public Instant getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(Instant createdOn) { + this.createdOn = createdOn; + } + + @DynamoDbUpdateBehavior(WRITE_ALWAYS) + public Instant getLastUpdatedOn() { + return lastUpdatedOn; + } + + public void setLastUpdatedOn(Instant lastUpdatedOn) { + this.lastUpdatedOn = lastUpdatedOn; + } + + @DynamoDbVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +}