diff --git a/docs/guide/src/docs/asciidoc/dynamodb.adoc b/docs/guide/src/docs/asciidoc/dynamodb.adoc index 7581336c9..ce0307e59 100644 --- a/docs/guide/src/docs/asciidoc/dynamodb.adoc +++ b/docs/guide/src/docs/asciidoc/dynamodb.adoc @@ -581,6 +581,7 @@ include::{root-dir}/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy <8> You can combine `@SortKey` and `@Filter` annotations to specify the sort key condition <9> Only`EQ`, `LE`, `LT`, `GE`, `GT`, `BETWEEN` and `BEGINS_WITH` operators are supported <10> You can also use `@Filter` annotation to specify the sort key name +<11> If you use any customization annotations on delete method, then the method will be used as batch delete method TIP: The operator `EQ` is used by default if `@Filter` annotation is not present. This makes it special and the service introduction tries to find the appropriate operation based on the actual value. For collections or arrays, `inList` operation is actually used. If the actual value is `null` then `isNull` operation is used. For other types, `eq` operation is used. For sort keys, `eq` operation is always used. diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/ServiceIntroduction.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/ServiceIntroduction.java index dfd4763bf..f4022cf58 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/ServiceIntroduction.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/ServiceIntroduction.java @@ -49,6 +49,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; /** @@ -62,6 +63,11 @@ public class ServiceIntroduction implements MethodInterceptor { private static final String HASH = "hash"; private static final String RANGE = "range"; + private static class ItemArgument { + Argument argument; + boolean single; + } + private static class FilterArgument { Argument firstArgument; Argument secondArgument; @@ -156,7 +162,7 @@ private Object doIntercept(MethodInvocationContext context, private Object doIntercept(MethodInvocationContext context, DynamoDbService service) { String methodName = context.getMethodName(); if (methodName.startsWith("save")) { - return handleSave(service, context); + return handleSave(service, context, findItemArgument(service, context)); } if (methodName.startsWith("get") || methodName.startsWith("load")) { @@ -211,22 +217,36 @@ private Object doIntercept(MethodInvocationContext context, } if (methodName.startsWith("delete")) { - return handleDelete(service, context); + Optional maybeItemArgument = findItemArgument(service, context); + if (maybeItemArgument.isPresent()) { + return handleDelete(service, context, maybeItemArgument); + } } - if (methodName.startsWith("query") || methodName.startsWith("findAll") || methodName.startsWith("list") || methodName.startsWith("count")) { + if (methodName.startsWith("query") || methodName.startsWith("findAll") || methodName.startsWith("list") || methodName.startsWith("count") || methodName.startsWith("delete")) { String index = context.getTargetMethod().isAnnotationPresent(Index.class) ? context.getTargetMethod().getAnnotation(Index.class).value() : null; boolean consistent = context.getTargetMethod().isAnnotationPresent(Consistent.class) && context.getTargetMethod().getAnnotation(Consistent.class).value(); boolean descending = context.getTargetMethod().isAnnotationPresent(Descending.class) && context.getTargetMethod().getAnnotation(Descending.class).value(); QueryArguments partitionAndSort = findHashAndRange(context.getArguments(), service); + boolean customized = index != null || consistent || descending || !partitionAndSort.filters.isEmpty() || partitionAndSort.sortKey != null && partitionAndSort.sortKey.operator != Filter.Operator.EQ; + if (methodName.startsWith("count")) { - if (index != null || consistent || descending || !partitionAndSort.filters.isEmpty() || partitionAndSort.sortKey != null && partitionAndSort.sortKey.operator != Filter.Operator.EQ) { + if (customized) { return service.countUsingQuery(generateQuery(context, partitionAndSort, index, consistent, descending)); } return service.count(partitionAndSort.getPartitionValue(context.getParameters()), partitionAndSort.getSortValue(context.getParameters())); } - if (index != null || consistent || descending || !partitionAndSort.filters.isEmpty() || partitionAndSort.sortKey != null && partitionAndSort.sortKey.operator != Filter.Operator.EQ) { + + if (methodName.startsWith("delete")) { + Optional maybeItemArgument = findItemArgument(service, context); + if (customized) { + return service.deleteAll(service.query(generateQuery(context, partitionAndSort, index, consistent, descending))); + } + return handleDelete(service, context, maybeItemArgument); + } + + if (customized) { return publisherOrIterable(service.query(generateQuery(context, partitionAndSort, index, consistent, descending)), context.getReturnType().getType()); } return publisherOrIterable( @@ -246,41 +266,35 @@ private Object publisherOrIterable(Publisher result, Class type) { return Flux.from(result).collectList().blockOptional().orElse(Collections.emptyList()); } - private Object handleSave(DynamoDbService service, MethodInvocationContext context) { + @SuppressWarnings("unchecked") + private Object handleSave(DynamoDbService service, MethodInvocationContext context, Optional maybeItemArgument) { Map> params = context.getParameters(); - Argument[] args = context.getArguments(); - if (args.length != 1) { - throw new UnsupportedOperationException("Method expects 1 parameter - item, iterable of items or array of items"); - } - - Argument itemArgument = args[0]; - Publisher items = toPublisher(service.getItemType(), itemArgument, params); - - if (itemArgument.getType().isArray() || Iterable.class.isAssignableFrom(itemArgument.getType()) || Publisher.class.isAssignableFrom(itemArgument.getType())) { + if (maybeItemArgument.isPresent()) { + ItemArgument itemArgument = maybeItemArgument.get(); + Publisher items = toPublisher(service.getItemType(), itemArgument.argument, params); + if (itemArgument.single) { + return service.save((T) params.get(itemArgument.argument.getName()).getValue()); + } return publisherOrIterable(service.saveAll(items), context.getReturnType().getType()); + } else { + throw new UnsupportedOperationException("Method expects 1 parameter - item, iterable of items or array of items"); } - - return service.save((T) params.get(itemArgument.getName()).getValue()); } - private Object handleDelete(DynamoDbService service, MethodInvocationContext context) { + private Object handleDelete(DynamoDbService service, MethodInvocationContext context, Optional maybeItemArgument) { Map> params = context.getParameters(); - Argument[] args = context.getArguments(); - if (args.length == 1) { - Argument itemArgument = args[0]; - Publisher items = toPublisher(service.getItemType(), itemArgument, params); - - if (itemArgument.getType().isArray() || Iterable.class.isAssignableFrom(itemArgument.getType()) || Publisher.class.isAssignableFrom(itemArgument.getType())) { - return service.deleteAll(items); - } - - if (service.getItemType().isAssignableFrom(itemArgument.getType())) { + if (maybeItemArgument.isPresent()) { + ItemArgument itemArgument = maybeItemArgument.get(); + Publisher items = toPublisher(service.getItemType(), itemArgument.argument, params); + if (itemArgument.single) { return service.delete(Flux.from(items).blockFirst()); } + return service.deleteAll(items); } + Argument[] args = context.getArguments(); if (args.length > 2) { throw new UnsupportedOperationException("Method expects at most 2 parameters - partition key and sort key, an item or items"); } @@ -317,6 +331,30 @@ private Object handleGet(DynamoDbService service, MethodInvocationContext return service.get(partitionValue, partitionAndSort.getSortValue(params)); } + private Optional findItemArgument(DynamoDbService service, MethodInvocationContext context) { + Map> params = context.getParameters(); + Argument[] args = context.getArguments(); + + if (args.length == 1) { + Argument itemArgument = args[0]; + if (itemArgument.getType().isArray() || Iterable.class.isAssignableFrom(itemArgument.getType()) || Publisher.class.isAssignableFrom(itemArgument.getType())) { + ItemArgument item = new ItemArgument(); + item.argument = itemArgument; + item.single = false; + return Optional.of(item); + } + + if (service.getItemType().isAssignableFrom(itemArgument.getType())) { + ItemArgument item = new ItemArgument(); + item.argument = itemArgument; + item.single = true; + return Optional.of(item); + } + } + + return Optional.empty(); + } + private QueryArguments findHashAndRange(Argument[] arguments, DynamoDbService table) { QueryArguments names = new QueryArguments(); for (Argument argument : arguments) { diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/AdvancedQueryOnDeclarativeServiceTest.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/AdvancedQueryOnDeclarativeServiceTest.java index e61cb283a..d19919f6c 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/AdvancedQueryOnDeclarativeServiceTest.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/AdvancedQueryOnDeclarativeServiceTest.java @@ -69,6 +69,8 @@ public void testJavaService() { assertEquals(4, s.findAllByNumberInExplicit("1", List.of(1, 2)).size()); assertEquals(4, s.findAllByNumberBetween("1", 1, 2).size()); assertEquals(4, s.findAllByRangeBeginsWith("1", "f").size()); + assertEquals(4, s.deleteAllByRangeBeginsWith("1", "f")); + assertEquals(0, s.findAllByRangeBeginsWith("1", "f").size()); } diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy index 61adfb7bd..ffc5d6a96 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy @@ -71,6 +71,7 @@ class DefaultDynamoDBServiceSpec extends Specification { } // end::setup[] + @SuppressWarnings('LineLength') void 'unsupported methods throws meaningful messages'() { when: unknownMethodsService.doSomething() @@ -87,8 +88,8 @@ class DefaultDynamoDBServiceSpec extends Specification { when: unknownMethodsService.delete('1', '1', '1') then: - UnsupportedOperationException e3 = thrown(UnsupportedOperationException) - e3.message == 'Method expects at most 2 parameters - partition key and sort key, an item or items' + IllegalArgumentException e3 = thrown(IllegalArgumentException) + e3.message == '''Unknown property somethingElse for DynamoDBEntity{parentId='null', id='null', rangeIndex='null', date=null, number=0, mapProperty={}}''' when: unknownMethodsService.get('1', '1', '1') diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntityService.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntityService.java index e45367ec8..6ad0bb83d 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntityService.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntityService.java @@ -215,6 +215,17 @@ List findAllByRangeBeginsWith( ) String rangeIndexPrefix ); + + @Index(DynamoDBEntity.RANGE_INDEX) + int deleteAllByRangeBeginsWith( // <11> + @PartitionKey String parentId, + @SortKey + @Filter( + value = Filter.Operator.BEGINS_WITH, + name = "rangeIndex" + ) + String rangeIndexPrefix + ); // end::advanced-query-methods[] // CHECKSTYLE:ON