From 7c34b5b1365c62dab8470d316e8985f4508844b7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Jan 2025 14:20:40 +0100 Subject: [PATCH] Support delete methods --- .../mongodb/aot/generated/MongoBlocks.java | 86 +++++++++++++++++-- .../generated/MongoRepositoryContributor.java | 20 +++-- .../repository/query/MongoQueryExecution.java | 37 ++++++++ .../test/java/example/aot/UserRepository.java | 18 +++- .../MongoRepositoryContributorTests.java | 44 ++++++++++ 5 files changed, 191 insertions(+), 14 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java index 653ec17b4..1f550d814 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -24,17 +24,22 @@ import java.util.stream.Collectors; import org.bson.Document; import org.springframework.data.mongodb.BindableMongoExpression; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX.Type; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -45,23 +50,84 @@ public class MongoBlocks { private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); - public static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { + static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { return new QueryBlockBuilder(context); } - public static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { return new QueryExecutionBlockBuilder(context); } + static DeleteExecutionBuilder deleteExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new DeleteExecutionBuilder(context); + } + + static class DeleteExecutionBuilder { + + AotRepositoryMethodGenerationContext context; + String queryVariableName; + + public DeleteExecutionBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + } + + public DeleteExecutionBuilder referencing(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + public CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); + + builder.add("\n"); + builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, actualReturnType, + mongoOpsRef, context.getRepositoryInformation().getDomainType()); + + Type type = Type.FIND_AND_REMOVE_ALL; + if (context.returnsSingleValue()) { + if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) { + type = Type.FIND_AND_REMOVE_ONE; + } else { + type = Type.ALL; + } + } + + actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) + ? ClassName.get(context.getMethod().getReturnType()) + : context.returnsSingleValue() ? actualReturnType : context.getReturnType(); + + builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecutionX.class, + DeleteExecutionX.Type.class, type.name(), queryVariableName); + + return builder.build(); + } + } + static class QueryExecutionBlockBuilder { AotRepositoryMethodGenerationContext context; + private String queryVariableName; public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; } - CodeBlock build(String queryVariableName) { + QueryExecutionBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { String mongoOpsRef = context.fieldNameOf(MongoOperations.class); @@ -74,10 +140,12 @@ public class MongoBlocks { : context.getRepositoryInformation().getDomainType(); builder.add("\n"); + if (isProjecting) { builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); } else { + builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, context.getRepositoryInformation().getDomainType()); } @@ -97,7 +165,6 @@ public class MongoBlocks { } if (context.returnsPage()) { - // builder.addStatement("return finder.$L", terminatingMethod); builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, context.getPageableParameterName(), queryVariableName); } else if (context.returnsSlice()) { @@ -110,7 +177,6 @@ public class MongoBlocks { return builder.build(); } - } static class QueryBlockBuilder { @@ -118,7 +184,8 @@ public class MongoBlocks { AotRepositoryMethodGenerationContext context; StringQuery source; List arguments; - + private String queryVariableName; + public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName) @@ -134,7 +201,12 @@ public class MongoBlocks { return this; } - CodeBlock build(String queryVariableName) { + public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { CodeBlock.Builder builder = CodeBlock.builder(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index 85d3bfa01..d42afd61b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.aot.generated; import java.util.regex.Pattern; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mongodb.aot.generated.MongoBlocks.QueryBlockBuilder; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Query; @@ -55,10 +56,6 @@ public class MongoRepositoryContributor extends RepositoryContributor { // TODO: do not generate stuff for spel expressions - // skip currently unsupported Stuff. - if (generationContext.isDeleteMethod()) { - return null; - } if (AnnotatedElementUtils.hasAnnotation(generationContext.getMethod(), Aggregation.class)) { return null; } @@ -100,8 +97,19 @@ public class MongoRepositoryContributor extends RepositoryContributor { private static void writeStringQuery(AotRepositoryMethodGenerationContext context, Builder body, StringQuery query) { body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - body.addCode(MongoBlocks.queryBlockBuilder(context).filter(query).build("query")); - body.addCode(MongoBlocks.queryExecutionBlockBuilder(context).build("query")); + QueryBlockBuilder queryBlockBuilder = MongoBlocks.queryBlockBuilder(context).filter(query); + + if (context.isDeleteMethod()) { + + String deleteQueryVariableName = "deleteQuery"; + body.addCode(queryBlockBuilder.usingQueryVariableName(deleteQueryVariableName).build()); + body.addCode(MongoBlocks.deleteExecutionBlockBuilder(context).referencing(deleteQueryVariableName).build()); + } else { + + String filterQueryVariableName = "filterQuery"; + body.addCode(queryBlockBuilder.usingQueryVariableName(filterQueryVariableName).build()); + body.addCode(MongoBlocks.queryExecutionBlockBuilder(context).referencing(filterQueryVariableName).build()); + } } private static void userAnnotatedQuery(AotRepositoryMethodGenerationContext context, Builder body, Query query) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index f1cbff588..60de94528 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.Iterator; import java.util.List; import java.util.function.Supplier; @@ -31,6 +32,9 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.NearQuery; @@ -242,6 +246,39 @@ public interface MongoQueryExecution { } } + final class DeleteExecutionX implements MongoQueryExecution { + + ExecutableRemoveOperation.ExecutableRemove remove; + Type type; + + public DeleteExecutionX(ExecutableRemove remove, Type type) { + this.remove = remove; + this.type = type; + } + + @Nullable + @Override + public Object execute(Query query) { + + TerminatingRemove doRemove = remove.matching(query); + if (Type.ALL.equals(type)) { + DeleteResult result = doRemove.all(); + return result.wasAcknowledged() ? Long.valueOf(result.getDeletedCount()) : Long.valueOf(0); + } else if (Type.FIND_AND_REMOVE_ALL.equals(type)) { + return doRemove.findAndRemove(); + } else if (Type.FIND_AND_REMOVE_ONE.equals(type)) { + Iterator removed = doRemove.findAndRemove().iterator(); + return removed.hasNext() ? removed.next() : null; + + } + throw new RuntimeException(); + } + + public enum Type { + FIND_AND_REMOVE_ONE, FIND_AND_REMOVE_ALL, ALL + } + } + /** * {@link MongoQueryExecution} removing documents matching the query. * diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 8a796e52f..104fd8d08 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -104,7 +104,23 @@ public interface UserRepository extends CrudRepository { @Query("{ 'lastname' : { '$regex' : '^?0' } }") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); - // TODO: deletes + /* deletes */ + + User deleteByUsername(String username); + + @Query(value = "{ 'username' : ?0 }", delete = true) + User deleteAnnotatedQueryByUsername(String username); + + Long deleteByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + Long deleteAnnotatedQueryByLastnameStartingWith(String lastname); + + List deleteUsersByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + List deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname); + // TODO: updates // TODO: Aggregations diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java index 4e0d8bb7c..9caf74f31 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -51,6 +51,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -388,6 +390,48 @@ public class MongoRepositoryContributorTests { }); } + @ParameterizedTest + @ValueSource(strings = { "deleteByUsername", "deleteAnnotatedQueryByUsername" }) + void testDeleteSingle(String methodName) { + + generated.verify(methodInvoker -> { + + User result = methodInvoker.invoke(methodName, "yoda").onBean("aotUserRepository"); + + assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); + } + + @ParameterizedTest + @ValueSource(strings = { "deleteByLastnameStartingWith", "deleteAnnotatedQueryByLastnameStartingWith" }) + void testDerivedDeleteMultipleReturningDeleteCount(String methodName) { + + generated.verify(methodInvoker -> { + + Long result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); + + assertThat(result).isEqualTo(4L); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @ParameterizedTest + @ValueSource(strings = { "deleteUsersByLastnameStartingWith", "deleteUsersAnnotatedQueryByLastnameStartingWith" }) + void testDerivedDeleteMultipleReturningDeleted(String methodName) { + + generated.verify(methodInvoker -> { + + List result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); + + assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + @Test void testDerivedFinderWithAnnotatedSort() {