diff --git a/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbOperations.java b/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbOperations.java index 6fba88524..febb30513 100644 --- a/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbOperations.java +++ b/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbOperations.java @@ -17,9 +17,12 @@ import org.springframework.lang.Nullable; import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; /** * Interface for simple DynamoDB template operations. @@ -37,6 +40,17 @@ public interface DynamoDbOperations { */ T save(T entity); + /** + * Saves an item in DynamoDB using the provided PutItemEnhancedRequest. + * + * @param putItemEnhancedRequest the request object containing the item to be saved + * @param clazz the class type of the item to be saved so + * {@link software.amazon.awssdk.enhanced.dynamodb.TableSchema} can be generated. + * + * @see PutItemEnhancedRequest + */ + void save(PutItemEnhancedRequest putItemEnhancedRequest, Class clazz); + /** * Updates Entity to DynamoDB table. * @@ -45,6 +59,17 @@ public interface DynamoDbOperations { */ T update(T entity); + /** + * Updates an item in DynamoDB using the provided UpdateItemEnhancedRequest. + * + * @param updateItemEnhancedRequest the request object containing the item to be updated + * @param clazz the class type of the item to be updated so + * {@link software.amazon.awssdk.enhanced.dynamodb.TableSchema} can be generated. + * + * @see UpdateItemEnhancedRequest + */ + T update(UpdateItemEnhancedRequest updateItemEnhancedRequest, Class clazz); + /** * Deletes a record for a given Key. * @@ -61,6 +86,17 @@ public interface DynamoDbOperations { */ T delete(T entity); + /** + * Deletes a record for a given DeleteItemEnhancedRequest. + * + * @param deleteItemEnhancedRequest the request object containing the item to be deleted + * @param clazz the class type of the item to be deleted so + * {@link software.amazon.awssdk.enhanced.dynamodb.TableSchema} can be generated. + * + * @see DeleteItemEnhancedRequest + */ + T delete(DeleteItemEnhancedRequest deleteItemEnhancedRequest, Class clazz); + /** * Loads entity for a given Key. * diff --git a/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbTemplate.java b/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbTemplate.java index 510eea6d9..d81f87010 100644 --- a/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbTemplate.java +++ b/spring-cloud-aws-dynamodb/src/main/java/io/awspring/cloud/dynamodb/DynamoDbTemplate.java @@ -21,9 +21,12 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; /** * Default implementation of {@link DynamoDbOperations}. @@ -61,11 +64,23 @@ public T save(T entity) { return entity; } + public void save(PutItemEnhancedRequest putItemEnhancedRequest, Class clazz) { + Assert.notNull(putItemEnhancedRequest, "putItemEnhancedRequest is required"); + Assert.notNull(clazz, "clazz is required"); + prepareTable(clazz).putItem(putItemEnhancedRequest); + } + public T update(T entity) { Assert.notNull(entity, "entity is required"); return prepareTable(entity).updateItem(entity); } + public T update(UpdateItemEnhancedRequest updateItemEnhancedRequest, Class clazz) { + Assert.notNull(updateItemEnhancedRequest, "updateItemEnhancedRequest is required"); + Assert.notNull(clazz, "clazz is required"); + return prepareTable(clazz).updateItem(updateItemEnhancedRequest); + } + public T delete(Key key, Class clazz) { Assert.notNull(key, "key is required"); Assert.notNull(clazz, "clazz is required"); @@ -77,6 +92,12 @@ public T delete(T entity) { return prepareTable(entity).deleteItem(entity); } + public T delete(DeleteItemEnhancedRequest deleteItemEnhancedRequest, Class clazz) { + Assert.notNull(deleteItemEnhancedRequest, "deleteItemEnhancedRequest is required"); + Assert.notNull(clazz, "clazz is required"); + return prepareTable(clazz).deleteItem(deleteItemEnhancedRequest); + } + @Nullable public T load(Key key, Class clazz) { Assert.notNull(key, "key is required"); diff --git a/spring-cloud-aws-dynamodb/src/test/java/io/awspring/cloud/dynamodb/DynamoDbTemplateIntegrationTest.java b/spring-cloud-aws-dynamodb/src/test/java/io/awspring/cloud/dynamodb/DynamoDbTemplateIntegrationTest.java index d7f121506..7afcfad9e 100644 --- a/spring-cloud-aws-dynamodb/src/test/java/io/awspring/cloud/dynamodb/DynamoDbTemplateIntegrationTest.java +++ b/spring-cloud-aws-dynamodb/src/test/java/io/awspring/cloud/dynamodb/DynamoDbTemplateIntegrationTest.java @@ -16,6 +16,7 @@ package io.awspring.cloud.dynamodb; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; import java.util.ArrayList; import java.util.List; @@ -33,10 +34,13 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.enhanced.dynamodb.*; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.*; @@ -310,6 +314,131 @@ void dynamoDbTemplate_saveAndScanForParticularIndex_entitySuccessful(DynamoDbTab cleanUp(dynamoDbTable, personEntity2.getUuid()); } + @ParameterizedTest + @MethodSource("argumentSource") + void dynamoDbTemplate_saveConditionallyAndRead_entitySuccessfully(DynamoDbTable dynamoDbTable, + DynamoDbTemplate dynamoDbTemplate) { + UUID uuid = UUID.randomUUID(); + PersonEntity personEntity = new PersonEntity(uuid, "foo", null); + PersonEntity secondPersonEntity = new PersonEntity(uuid, "foo", "jar"); + PutItemEnhancedRequest putItemEnhancedRequest = PutItemEnhancedRequest.builder(PersonEntity.class) + .conditionExpression(Expression.builder().expression("attribute_not_exists(lastName)").build()) + .item(secondPersonEntity).build(); + // save a person with lastName "bar" + dynamoDbTemplate.save(personEntity); + // attempt to replace person with lastName "jar" + dynamoDbTemplate.save(putItemEnhancedRequest, PersonEntity.class); + PersonEntity savedPersonEntity = dynamoDbTemplate.load( + Key.builder().partitionValue(secondPersonEntity.getUuid().toString()).build(), PersonEntity.class); + assertThat(savedPersonEntity).isEqualTo(secondPersonEntity); + + cleanUp(dynamoDbTable, personEntity.getUuid()); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void dynamoDbTemplate_saveConditionally_entityFails(DynamoDbTable dynamoDbTable, + DynamoDbTemplate dynamoDbTemplate) { + UUID uuid = UUID.randomUUID(); + PersonEntity personEntity = new PersonEntity(uuid, "foo", "bar"); + PersonEntity secondPersonEntity = new PersonEntity(uuid, "foo", "jar"); + PutItemEnhancedRequest putItemEnhancedRequest = PutItemEnhancedRequest.builder(PersonEntity.class) + .conditionExpression(Expression.builder().expression("attribute_not_exists(lastName)").build()) + .item(secondPersonEntity).build(); + // save person with lastName "bar" + dynamoDbTemplate.save(personEntity); + // try to save new lastName "jar" for the same person + assertThrows(DynamoDbException.class, () -> { + dynamoDbTemplate.save(putItemEnhancedRequest, PersonEntity.class); + }); + + cleanUp(dynamoDbTable, personEntity.getUuid()); + + } + + @ParameterizedTest + @MethodSource("argumentSource") + void dynamoDbTemplate_updateConditionally_entitySuccessfully(DynamoDbTable dynamoDbTable, + DynamoDbTemplate dynamoDbTemplate) { + UUID uuid = UUID.randomUUID(); + PersonEntity personEntity = new PersonEntity(uuid, "foo", null); + PersonEntity updatedPersonEntity = new PersonEntity(uuid, "foo", "bar"); + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest + .builder(PersonEntity.class) + .conditionExpression(Expression.builder().expression("attribute_not_exists(lastName)").build()) + .item(updatedPersonEntity).build(); + // save a person with lastName "bar" + dynamoDbTemplate.save(personEntity); + // update person lastName + PersonEntity savedPersonEntity = dynamoDbTemplate.update(updateItemEnhancedRequest, PersonEntity.class); + assertThat(savedPersonEntity.getLastName()).isEqualTo("bar"); + + cleanUp(dynamoDbTable, personEntity.getUuid()); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void dynamoDbTemplate_updateConditionally_entityFails(DynamoDbTable dynamoDbTable, + DynamoDbTemplate dynamoDbTemplate) { + UUID uuid = UUID.randomUUID(); + PersonEntity personEntity = new PersonEntity(uuid, "foo", "bar"); + PersonEntity updatedPersonEntity = new PersonEntity(uuid, "foo", "jar"); + UpdateItemEnhancedRequest updateItemEnhancedRequest = UpdateItemEnhancedRequest + .builder(PersonEntity.class) + .conditionExpression(Expression.builder().expression("attribute_not_exists(lastName)").build()) + .item(updatedPersonEntity).build(); + // save a person with lastName "bar" + dynamoDbTemplate.save(personEntity); + // update person lastName + assertThrows(DynamoDbException.class, () -> { + dynamoDbTemplate.update(updateItemEnhancedRequest, PersonEntity.class); + }); + cleanUp(dynamoDbTable, personEntity.getUuid()); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void dynamoDbTemplate_deleteConditionally_entitySuccessfully(DynamoDbTable dynamoDbTable, + DynamoDbTemplate dynamoDbTemplate) { + UUID uuid = UUID.randomUUID(); + PersonEntity personEntity = new PersonEntity(uuid, "notfoo", "bar"); + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .conditionExpression(Expression.builder().expression("#nameNotBeDeleted <> :value") + .putExpressionName("#nameNotBeDeleted", "name") + .putExpressionValue(":value", AttributeValue.builder().s("foo").build()).build()) + .key(Key.builder().partitionValue(personEntity.getUuid().toString()).build()).build(); + dynamoDbTemplate.save(personEntity); + dynamoDbTemplate.delete(deleteItemEnhancedRequest, PersonEntity.class); + + PersonEntity deletedEntity = dynamoDbTemplate + .load(Key.builder().partitionValue(personEntity.getUuid().toString()).build(), PersonEntity.class); + + assertThat(deletedEntity).isNull(); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void dynamoDbTemplate_deleteConditionally_entityFails(DynamoDbTable dynamoDbTable, + DynamoDbTemplate dynamoDbTemplate) { + UUID uuid = UUID.randomUUID(); + PersonEntity personEntity = new PersonEntity(uuid, "foo", "bar"); + DeleteItemEnhancedRequest deleteItemEnhancedRequest = DeleteItemEnhancedRequest.builder() + .conditionExpression(Expression.builder().expression("#nameNotBeDeleted <> :value") + .putExpressionName("#nameNotBeDeleted", "name") + .putExpressionValue(":value", AttributeValue.builder().s("foo").build()).build()) + .key(Key.builder().partitionValue(personEntity.getUuid().toString()).build()).build(); + dynamoDbTemplate.save(personEntity); + assertThrows(DynamoDbException.class, () -> { + dynamoDbTemplate.delete(deleteItemEnhancedRequest, PersonEntity.class); + }); + + PersonEntity deletedEntity = dynamoDbTemplate + .load(Key.builder().partitionValue(personEntity.getUuid().toString()).build(), PersonEntity.class); + + assertThat(deletedEntity).isNotNull(); + cleanUp(dynamoDbTable, personEntity.getUuid()); + } + public static void cleanUp(DynamoDbTable dynamoDbTable, UUID uuid) { dynamoDbTable.deleteItem(Key.builder().partitionValue(uuid.toString()).build()); }