From 1d91d1d9abe06fc244f900febd9fbdad5d03bcb5 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 22 Jul 2025 14:54:53 +0200 Subject: [PATCH 01/12] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- spring-data-mongodb/src/test/resources/logback.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 9a1889723d..1cf8761bdc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5027-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..fb60c00423 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5027-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 595e5a4250..c41ad60ea5 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5027-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 0bc943eafd..fbb189a66b 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -21,7 +21,7 @@ + level="trace"/> From 35ba0718d28a5677068dbea12241aaf9a76b8ca8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 21 Jul 2025 16:22:17 +0200 Subject: [PATCH 02/12] tmp save --- .../repository/aot/AggregationBlocks.java | 31 ++++++-- .../MongoAotRepositoryFragmentSupport.java | 70 +++++++++++++------ .../repository/aot/MongoCodeBlocks.java | 2 +- .../aot/MongoRepositoryContributor.java | 6 +- .../mongodb/repository/aot/QueryBlocks.java | 3 +- .../AotPersonRepositoryIntegrationTests.java | 2 +- .../mongodb/repository/PersonAggregate.java | 2 +- 7 files changed, 83 insertions(+), 33 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index e8dbffb19a..9335333079 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -42,6 +42,7 @@ import org.springframework.data.util.ReflectionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.lang.NonNull; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -80,12 +81,7 @@ CodeBlock build() { builder.add("\n"); - Class outputType = queryMethod.getReturnedObjectType(); - if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) { - outputType = Document.class; - } else if (ClassUtils.isAssignable(AggregationResults.class, outputType)) { - outputType = queryMethod.getReturnType().getComponentType().getType(); - } + Class outputType = getOutputType(queryMethod); if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) { builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); @@ -155,6 +151,18 @@ CodeBlock build() { return builder.build(); } + + + } + + private static Class getOutputType(MongoQueryMethod queryMethod) { + Class outputType = queryMethod.getReturnedObjectType(); + if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) { + outputType = Document.class; + } else if (ClassUtils.isAssignable(AggregationResults.class, outputType)) { + outputType = queryMethod.getReturnType().getComponentType().getType(); + } + return outputType; } @NullUnmarked @@ -242,8 +250,17 @@ private CodeBlock pipeline(String pipelineVariableName) { builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery())); } - builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, + Class outputType = getOutputType(queryMethod); + if(outputType.equals(Document.class) || outputType.equals(context.getRepositoryInformation().getDomainType())) { + builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, context.localVariable("stages")); + } else { + builder.addStatement("$T $L = createPipeline($L, $T.class)", AggregationPipeline.class, pipelineVariableName, + context.localVariable("stages"), outputType); + } + + + return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index b7f65df1a8..63c155e8c8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -24,7 +24,6 @@ import org.bson.Document; import org.jspecify.annotations.Nullable; - import org.springframework.data.domain.Range; import org.springframework.data.domain.Score; import org.springframework.data.domain.ScoringFunction; @@ -56,6 +55,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.Lazy; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ObjectUtils; @@ -116,12 +116,12 @@ protected Document bindParameters(Method method, String source, Object... args) ParameterBindingContext bindingContext = new ParameterBindingContext(parametersParameterAccessor::getBindableValue, new ValueExpressionEvaluator() { - @Override - @SuppressWarnings("unchecked") - public @Nullable T evaluate(String expression) { + @Override + @SuppressWarnings("unchecked") + public @Nullable T evaluate(String expression) { return (T) MongoAotRepositoryFragmentSupport.this.evaluate(method, expression, args); - } - }); + } + }); return CODEC.decode(source, bindingContext); } @@ -231,26 +231,54 @@ protected BasicQuery createQuery(Method method, String queryString, Object... pa } protected AggregationPipeline createPipeline(List rawStages) { + return createPipeline(rawStages, null); + } - List stages = new ArrayList<>(rawStages.size()); - boolean first = true; - for (Object rawStage : rawStages) { - if (rawStage instanceof Document stageDocument) { - if (first) { - stages.add((ctx) -> ctx.getMappedObject(stageDocument)); - } else { - stages.add((ctx) -> stageDocument); + protected AggregationPipeline createPipeline(List rawStages, @Nullable Class projection) { + + if (rawStages.isEmpty()) { + return new AggregationPipeline(List.of()); + } + + int size = rawStages.size(); + List stages = new ArrayList<>(size); + + Object firstElement = CollectionUtils.firstElement(rawStages); + stages.add(rawToAggregationOperation(firstElement, true, null)); + + if (size == 1) { + return new AggregationPipeline(stages); + } + + for (int i = 1; i < size - 1; i++) { + stages.add(rawToAggregationOperation(firstElement, false, null)); + } + + Object lastElement = CollectionUtils.lastElement(rawStages); + stages.add(rawToAggregationOperation(lastElement, true, projection)); + + return new AggregationPipeline(stages); + } + + private static AggregationOperation rawToAggregationOperation(Object rawStage, boolean requiresMapping, + @Nullable Class type) { + + if (rawStage instanceof Document stageDocument) { + if (requiresMapping) { + if (type != null) { + return (ctx) -> ctx.getMappedObject(stageDocument, type); } - } else if (rawStage instanceof AggregationOperation aggregationOperation) { - stages.add(aggregationOperation); + return (ctx) -> ctx.getMappedObject(stageDocument); } else { - throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass())); - } - if (first) { - first = false; + return (ctx) -> stageDocument; } } - return new AggregationPipeline(stages); + + if (rawStage instanceof AggregationOperation aggregationOperation) { + return aggregationOperation; + } + throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass())); + } protected List convertSimpleRawResults(Class targetType, List rawResults) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java index e887e0a118..c1261ab055 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -169,7 +169,7 @@ static CodeBlock asDocument(String source, String argNames) { if (!StringUtils.hasText(source)) { builder.add("new $T()", Document.class); } else if (containsPlaceholder(source)) { - builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S$L);\n", source, argNames); + builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S$L)", source, argNames); } else { builder.add("parse($S)", source); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index 96bc359241..a4a8422a78 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -237,8 +237,12 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor } else { PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); - query = new QueryInteraction(queryCreator.createQuery(partTree, queryMethod, source), + AotStringQuery aotStringQuery = queryCreator.createQuery(partTree, queryMethod, source); + query = new QueryInteraction(aotStringQuery, partTree.isCountProjection(), partTree.isDelete(), partTree.isExistsProjection()); +// if(partTree.isLimiting()) { +// query.s +// } } if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index 0f3697b303..a4d548fd86 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -104,6 +104,7 @@ CodeBlock build() { } if (queryMethod.isPageQuery()) { + builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"), context.getPageableParameterName(), query.name()); } else if (queryMethod.isSliceQuery()) { @@ -194,7 +195,7 @@ CodeBlock build() { String limitParameter = context.getLimitParameterName(); if (StringUtils.hasText(limitParameter)) { builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); - } else if (context.getPageableParameterName() == null && source.getQuery().isLimited()) { + } else if (source.getQuery().isLimited()) { builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit()); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index ce26b11c5e..605a127d5f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -34,7 +34,7 @@ * @author Mark Paluch */ @ContextConfiguration(classes = AotPersonRepositoryIntegrationTests.Config.class) -@Disabled("Several mismatches, some class-loader visibility issues and some behavioral differences remain to be fixed") +//@Disabled("Several mismatches, some class-loader visibility issues and some behavioral differences remain to be fixed") class AotPersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { @Configuration diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java index da22801ba6..9120042eca 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java @@ -28,7 +28,7 @@ * @author Christoph Strobl * @author Mark Paluch */ -final class PersonAggregate { +public final class PersonAggregate { @Id private final String lastname; private final Set names; From a67cdb743d957b86814c4eaad1f60122a7c4c4eb Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 22 Jul 2025 08:53:34 +0200 Subject: [PATCH 03/12] fix aggregation sort mapping --- .../repository/aot/AggregationBlocks.java | 34 +++++++++---------- .../MongoAotRepositoryFragmentSupport.java | 19 +++-------- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index 9335333079..99e505fef1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -22,13 +22,13 @@ import org.bson.Document; import org.jspecify.annotations.NullUnmarked; - import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.AggregationResults; @@ -42,7 +42,6 @@ import org.springframework.data.util.ReflectionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; -import org.springframework.lang.NonNull; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -152,7 +151,6 @@ CodeBlock build() { return builder.build(); } - } private static Class getOutputType(MongoQueryMethod queryMethod) { @@ -239,7 +237,8 @@ private CodeBlock pipeline(String pipelineVariableName) { builder.add(aggregationStages(context.localVariable("stages"), source.stages())); if (StringUtils.hasText(sortParameter)) { - builder.add(sortingStage(sortParameter)); + Class outputType = getOutputType(queryMethod); + builder.add(sortingStage(sortParameter, outputType)); } if (StringUtils.hasText(limitParameter)) { @@ -250,16 +249,8 @@ private CodeBlock pipeline(String pipelineVariableName) { builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery())); } - Class outputType = getOutputType(queryMethod); - if(outputType.equals(Document.class) || outputType.equals(context.getRepositoryInformation().getDomainType())) { - builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, + builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, context.localVariable("stages")); - } else { - builder.addStatement("$T $L = createPipeline($L, $T.class)", AggregationPipeline.class, pipelineVariableName, - context.localVariable("stages"), outputType); - } - - return builder.build(); } @@ -329,7 +320,7 @@ private CodeBlock aggregationStages(String stageListVariableName, Collection outputType) { Builder builder = CodeBlock.builder(); @@ -339,8 +330,17 @@ private CodeBlock sortingStage(String sortProvider) { builder.addStatement("$1L.append($2L.getProperty(), $2L.isAscending() ? 1 : -1);", context.localVariable("sortDocument"), context.localVariable("order")); builder.endControlFlow(); - builder.addStatement("stages.add(new $T($S, $L))", Document.class, "$sort", - context.localVariable("sortDocument")); + + if (outputType == Document.class || MongoSimpleTypes.HOLDER.isSimpleType(outputType) + || ClassUtils.isAssignable(context.getRepositoryInformation().getDomainType(), outputType)) { + builder.addStatement("$L.add(new $T($S, $L))", context.localVariable("stages"), Document.class, "$sort", + context.localVariable("sortDocument")); + } else { + builder.addStatement("$L.add(($T) _ctx -> new $T($S, _ctx.getMappedObject($L, $T.class)))", + context.localVariable("stages"), AggregationOperation.class, Document.class, "$sort", + context.localVariable("sortDocument"), outputType); + } + builder.endControlFlow(); return builder.build(); @@ -350,7 +350,7 @@ private CodeBlock pagingStage(String pageableProvider, boolean slice) { Builder builder = CodeBlock.builder(); - builder.add(sortingStage(pageableProvider + ".getSort()")); + builder.add(sortingStage(pageableProvider + ".getSort()", getOutputType(queryMethod))); builder.beginControlFlow("if ($L.isPaged())", pageableProvider); builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index 63c155e8c8..055e8e0742 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -231,10 +231,6 @@ protected BasicQuery createQuery(Method method, String queryString, Object... pa } protected AggregationPipeline createPipeline(List rawStages) { - return createPipeline(rawStages, null); - } - - protected AggregationPipeline createPipeline(List rawStages, @Nullable Class projection) { if (rawStages.isEmpty()) { return new AggregationPipeline(List.of()); @@ -244,30 +240,23 @@ protected AggregationPipeline createPipeline(List rawStages, @Nullable C List stages = new ArrayList<>(size); Object firstElement = CollectionUtils.firstElement(rawStages); - stages.add(rawToAggregationOperation(firstElement, true, null)); + stages.add(rawToAggregationOperation(firstElement, true)); if (size == 1) { return new AggregationPipeline(stages); } - for (int i = 1; i < size - 1; i++) { - stages.add(rawToAggregationOperation(firstElement, false, null)); + for (int i = 1; i < size; i++) { + stages.add(rawToAggregationOperation(rawStages.get(i), false)); } - Object lastElement = CollectionUtils.lastElement(rawStages); - stages.add(rawToAggregationOperation(lastElement, true, projection)); - return new AggregationPipeline(stages); } - private static AggregationOperation rawToAggregationOperation(Object rawStage, boolean requiresMapping, - @Nullable Class type) { + private static AggregationOperation rawToAggregationOperation(Object rawStage, boolean requiresMapping) { if (rawStage instanceof Document stageDocument) { if (requiresMapping) { - if (type != null) { - return (ctx) -> ctx.getMappedObject(stageDocument, type); - } return (ctx) -> ctx.getMappedObject(stageDocument); } else { return (ctx) -> stageDocument; From b7ca35d64931ca9aee5693bd7ded909253fa7a27 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 22 Jul 2025 12:13:07 +0200 Subject: [PATCH 04/12] regex and first vs one loading --- .../repository/aot/AotPlaceholders.java | 25 +++ .../repository/aot/AotQueryCreator.java | 16 +- .../mongodb/repository/aot/QueryBlocks.java | 22 ++- .../repository/query/MongoQueryCreator.java | 17 +- ...tractPersonRepositoryIntegrationTests.java | 6 +- .../AotPersonRepositoryIntegrationTests.java | 155 +++++++++++++++++- .../mongodb/repository/PersonRepository.java | 11 ++ .../data/mongodb/repository/SumAge.java | 2 +- 8 files changed, 229 insertions(+), 25 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index be30dcf357..84bc0b4d46 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -16,7 +16,9 @@ package org.springframework.data.mongodb.repository.aot; import java.util.List; +import java.util.regex.Pattern; +import kotlin.text.Regex; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -106,6 +108,10 @@ public static Shape polygon(int index) { return new PolygonPlaceholder(index); } + public static RegexPlaceholder regex(int index) { + return new RegexPlaceholder(index); + } + /** * A placeholder expression used when rending queries to JSON. * @@ -265,4 +271,23 @@ public String toString() { } + static class RegexPlaceholder implements Placeholder { + + private final int index; + + public RegexPlaceholder(int index) { + this.index = index; + } + + @Override + public String getValue() { + return "?" + index; + } + + @Override + public String toString() { + return getValue(); + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index d48983ba7e..2cb97f4fc1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -19,11 +19,11 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.regex.Pattern; import org.bson.conversions.Bson; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; - import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Score; @@ -47,6 +47,7 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.VectorSearch; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.Placeholder; +import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder; import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor; import org.springframework.data.mongodb.repository.query.MongoParameterAccessor; import org.springframework.data.mongodb.repository.query.MongoQueryCreator; @@ -117,6 +118,17 @@ protected Criteria regex(Criteria criteria, Object param) { protected Criteria exists(Criteria criteria, Object param) { return param instanceof Placeholder p ? criteria.raw("$exists", p) : super.exists(criteria, param); } + + @Override + protected Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria, + Object param) { + + if (param instanceof RegexPlaceholder) { + return criteria.raw("$regex", param); + } + + return super.createContainingCriteria(part, property, criteria, param); + } } static class PlaceholderConvertingParameterAccessor extends ConvertingParameterAccessor { @@ -176,6 +188,8 @@ public PlaceholderParameterAccessor(QueryMethod queryMethod) { placeholders.add(parameter.getIndex(), AotPlaceholders.sphere(parameter.getIndex())); } else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) { placeholders.add(parameter.getIndex(), AotPlaceholders.polygon(parameter.getIndex())); + } else if (ClassUtils.isAssignable(Pattern.class, parameter.getType())) { + placeholders.add(parameter.getIndex(), AotPlaceholders.regex(parameter.getIndex())); } else { placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index a4d548fd86..a6b275c81f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -19,7 +19,6 @@ import org.bson.Document; import org.jspecify.annotations.NullUnmarked; - import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.MongoOperations; @@ -100,7 +99,13 @@ CodeBlock build() { } else if (queryMethod.isStreamQuery()) { terminatingMethod = "stream()"; } else { - terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()"; + if (query.getQuery().isLimited()) { + terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "first()" + : "firstValue()"; + } else { + terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" + : "oneValue()"; + } } if (queryMethod.isPageQuery()) { @@ -182,16 +187,18 @@ CodeBlock build() { builder.addStatement("$L.setFieldsObject($L)", queryVariableName, fields.getVariableName()); } - String sortParameter = context.getSortParameterName(); - if (StringUtils.hasText(sortParameter)) { - builder.addStatement("$L.with($L)", queryVariableName, sortParameter); - } else if (StringUtils.hasText(source.getQuery().getSortString())) { + if (StringUtils.hasText(source.getQuery().getSortString())) { VariableSnippet sort = Snippet.declare(builder).variable(Document.class, context.localVariable("sort")) .of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), parameterNames)); builder.addStatement("$L.setSortObject($L)", queryVariableName, sort.getVariableName()); } + String sortParameter = context.getSortParameterName(); + if (StringUtils.hasText(sortParameter)) { + builder.addStatement("$L.with($L)", queryVariableName, sortParameter); + } + String limitParameter = context.getLimitParameterName(); if (StringUtils.hasText(limitParameter)) { builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); @@ -270,8 +277,7 @@ private CodeBlock renderExpressionToQuery() { Builder builder = CodeBlock.builder(); builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S$L)", source, parameterNames); return builder.build(); - } - else { + } else { return CodeBlock.of("new $T(parse($S))", BasicQuery.class, source); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 71d51f16e8..84d424458b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -205,11 +205,11 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit case STARTING_WITH: case ENDING_WITH: case CONTAINING: - return createContainingCriteria(part, property, criteria, parameters); + return createContainingCriteria(part, property, criteria, parameters.next()); case NOT_LIKE: - return createContainingCriteria(part, property, criteria.not(), parameters); + return createContainingCriteria(part, property, criteria.not(), parameters.next()); case NOT_CONTAINING: - return createContainingCriteria(part, property, criteria.not(), parameters); + return createContainingCriteria(part, property, criteria.not(), parameters.next()); case REGEX: return regex(criteria, parameters.next()); case EXISTS: @@ -344,18 +344,17 @@ private Criteria createLikeRegexCriteriaOrThrow(Part part, MongoPersistentProper * @param part * @param property * @param criteria - * @param parameters + * @param parameter * @return */ - private Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria, - Iterator parameters) { + protected Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria, + Object parameter) { if (property.isCollectionLike()) { - Object next = parameters.next(); - return in(criteria, part, next); + return in(criteria, part, parameter); } - return addAppropriateLikeRegexTo(criteria, part, parameters.next()); + return addAppropriateLikeRegexTo(criteria, part, parameter); } /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 493a23e4e5..bacd4b760c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -200,8 +200,7 @@ void findsPersonsByFirstnameLike() { @Test // DATAMONGO-1608 void findByFirstnameLikeWithNull() { - - assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameLike(null)); + assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameLike((String)null)); } @Test @@ -752,7 +751,6 @@ void executesGeoPageCountCorrectly() { @Test // DATAMONGO-1608 void findByFirstNameIgnoreCaseWithNull() { - assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameIgnoreCase(null)); } @@ -1662,7 +1660,7 @@ void executesQueryWithDocumentReferenceCorrectly() { @Test // GH-3656 @DirtiesState - void resultProjectionWithOptionalIsExcecutedCorrectly() { + void resultProjectionWithOptionalIsExecutedCorrectly() { carter.setAddress(new Address("batman", "robin", "gotham")); repository.save(carter); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index 605a127d5f..e41c758dcb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -15,16 +15,29 @@ */ package org.springframework.data.mongodb.repository; -import org.junit.jupiter.api.Disabled; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.TestMongoConfiguration; import org.springframework.data.mongodb.repository.aot.AotFragmentTestConfigurationSupport; import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; +import org.springframework.data.mongodb.test.util.DirtiesStateExtension.DirtiesState; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.test.context.ContextConfiguration; @@ -34,7 +47,8 @@ * @author Mark Paluch */ @ContextConfiguration(classes = AotPersonRepositoryIntegrationTests.Config.class) -//@Disabled("Several mismatches, some class-loader visibility issues and some behavioral differences remain to be fixed") +// @Disabled("Several mismatches, some class-loader visibility issues and some behavioral differences remain to be +// fixed") class AotPersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { @Configuration @@ -61,4 +75,141 @@ PersonRepository personRepository(MongoOperations mongoOperations, ApplicationCo } + @Test // GH-4397 + @Override + void executesFinderCorrectlyWithSortAndLimit() { + + List page = repository.findByLastnameLike(Pattern.compile(".*a.*"), + Sort.by(Direction.ASC, "lastname", "firstname"), Limit.of(2)); + + assertThat(page).containsExactly(carter, stefan); + } + + @Test + @Override + void findsPersonsByFirstnameLike() { + + List result = repository.findByFirstnameLike(Pattern.compile("Bo.*")); + assertThat(result).hasSize(1).contains(boyd); + } + + @Test // DATAMONGO-1424 + @Override + void findsPersonsByFirstnameNotLike() { + + List result = repository.findByFirstnameNotLike(Pattern.compile("Bo.*")); + assertThat(result).hasSize((int) (repository.count() - 1)); + assertThat(result).doesNotContain(boyd); + } + + @Test // GH-4308 + @Override + void appliesScrollPositionCorrectly() { + + Window page = repository.findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern.compile(".*a.*"), + ScrollPosition.keyset()); + + assertThat(page.isLast()).isFalse(); + assertThat(page.size()).isEqualTo(2); + assertThat(page).contains(carter); + } + + @Test // GH-4397 + @Override + void appliesLimitToScrollingCorrectly() { + + Window page = repository.findByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern.compile(".*a.*"), + ScrollPosition.keyset(), Limit.of(2)); + + assertThat(page.isLast()).isFalse(); + assertThat(page.size()).isEqualTo(2); + assertThat(page).contains(carter); + } + + @Test // GH-4308 + @Disabled + void appliesScrollPositionWithProjectionCorrectly() { + + Window page = repository.findCursorProjectionByLastnameLike(Pattern.compile(".*a.*"), + PageRequest.of(0, 2, Sort.by(Direction.ASC, "lastname", "firstname"))); + + assertThat(page.isLast()).isFalse(); + assertThat(page.size()).isEqualTo(2); + + assertThat(page).element(0).isEqualTo(new PersonSummaryDto(carter.getFirstname(), carter.getLastname())); + } + + @Test // DATADOC-236 + @Override + void appliesStaticAndDynamicSorting() { + List result = repository.findByFirstnameLikeOrderByLastnameAsc(Pattern.compile(".*e.*"), Sort.by("age")); + assertThat(result).hasSize(5); + assertThat(result.get(0)).isEqualTo(carter); + assertThat(result.get(1)).isEqualTo(stefan); + assertThat(result.get(2)).isEqualTo(oliver); + assertThat(result.get(3)).isEqualTo(dave); + assertThat(result.get(4)).isEqualTo(leroi); + } + + @Test // DATAMONGO-1608 + @Disabled + void findByFirstnameLikeWithNull() { + super.findByFirstnameLikeWithNull(); + } + + @Test // GH-3395 + @Disabled + void caseInSensitiveInClauseQuotesExpressions() { + super.caseInSensitiveInClauseQuotesExpressions(); + } + + @Test // GH-4839 + @Disabled + void annotatedAggregationWithAggregationResultAsClosedInterfaceProjection() { + super.annotatedAggregationWithAggregationResultAsClosedInterfaceProjection(); + } + + @Test // DATAMONGO-1608 + @Disabled + void findByFirstNameIgnoreCaseWithNull() { + super.findByFirstNameIgnoreCaseWithNull(); + } + + @Test // GH-3395 + @Disabled + void caseSensitiveInClauseIgnoresExpressions() { + super.caseSensitiveInClauseIgnoresExpressions(); + } + + @Test // GH-3656 + @DirtiesState + @Disabled + void resultProjectionWithOptionalIsExecutedCorrectly() { + super.resultProjectionWithOptionalIsExecutedCorrectly(); + } + + @Test + @Override + void executesPagedFinderCorrectly() { + + Page page = repository.findByLastnameLike(Pattern.compile(".*a.*"), + PageRequest.of(0, 2, Direction.ASC, "lastname", "firstname")); + assertThat(page.isFirst()).isTrue(); + assertThat(page.isLast()).isFalse(); + assertThat(page.getNumberOfElements()).isEqualTo(2); + assertThat(page).contains(carter, stefan); + } + + @Test // DATAMONGO-990 + @Disabled + void shouldFindByFirstnameAndCurrentUserWithCustomQuery() { + super.shouldFindByFirstnameAndCurrentUserWithCustomQuery(); + } + + @Test // GH-3395, GH-4404 + @Disabled + void caseInSensitiveInClause() { + super.caseInSensitiveInClause(); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 9ab0d71dc3..a9f694e200 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -98,6 +98,7 @@ public interface PersonRepository extends MongoRepository, Query * @return */ List findByFirstnameLike(@Nullable String firstname); + List findByFirstnameLike(Pattern firstname); List findByFirstnameNotContains(String firstname); @@ -108,8 +109,10 @@ public interface PersonRepository extends MongoRepository, Query * @return */ List findByFirstnameNotLike(String firstname); + List findByFirstnameNotLike(Pattern firstname); List findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort); + List findByFirstnameLikeOrderByLastnameAsc(Pattern firstname, Sort sort); List findBySkillsContains(List skills); @@ -128,8 +131,13 @@ public interface PersonRepository extends MongoRepository, Query Window findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition); + Window findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname, + ScrollPosition scrollPosition); + Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition, Limit limit); + Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname, + ScrollPosition scrollPosition, Limit limit); /** * Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards @@ -140,6 +148,7 @@ Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, * @return */ Window findCursorProjectionByLastnameLike(String lastname, Pageable pageable); + Window findCursorProjectionByLastnameLike(Pattern lastname, Pageable pageable); /** * Returns a page of {@link Person}s with a lastname matching the given one (*-wildcards supported). @@ -149,8 +158,10 @@ Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, * @return */ Page findByLastnameLike(String lastname, Pageable pageable); + Page findByLastnameLike(Pattern lastname, Pageable pageable); List findByLastnameLike(String lastname, Sort sort, Limit limit); + List findByLastnameLike(Pattern lastname, Sort sort, Limit limit); @Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}") Page findByLastnameLikeWithPageable(String lastname, Pageable pageable); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java index abbfac5943..c98c33d270 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java @@ -20,7 +20,7 @@ /** * @author Christoph Strobl */ -final class SumAge { +public final class SumAge { private final Long total; From f6d7ff7a956469cff4b433f62b43ac8fdcdceba1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 22 Jul 2025 14:53:36 +0200 Subject: [PATCH 05/12] interface projections for aggergations --- .../data/mongodb/core/MongoTemplate.java | 13 ++- .../repository/aot/AggregationBlocks.java | 1 - .../mongodb/repository/aot/QueryBlocks.java | 17 +++- .../core/aggregation/AggregationTests.java | 85 ++++++++++++++----- .../AotPersonRepositoryIntegrationTests.java | 7 -- 5 files changed, 91 insertions(+), 32 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index a49cd4ae98..895288d889 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -111,9 +111,11 @@ import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Lazy; import org.springframework.data.util.Optionals; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -2272,8 +2274,17 @@ protected AggregationResults doAggregate(Aggregation aggregation, String AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, QueryResultConverter resultConverter, AggregationOperationContext context) { - DocumentCallback callback = new QueryResultConverterCallback<>(resultConverter, + final DocumentCallback callback; + if(aggregation instanceof TypedAggregation ta && outputType.isInterface()) { + EntityProjection projection = operations.introspectProjection(outputType, ta.getInputType()); + ProjectingReadCallback cb = new ProjectingReadCallback(mongoConverter, projection, collectionName); + callback = new QueryResultConverterCallback<>(resultConverter, + cb); + } else { + + callback = new QueryResultConverterCallback<>(resultConverter, new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); + } AggregationOptions options = aggregation.getOptions(); AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index 99e505fef1..3b18bfe84f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -141,7 +141,6 @@ CodeBlock build() { builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); } else { - builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, aggregationVariableName, outputType); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index a6b275c81f..e79cf3fdae 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.jspecify.annotations.NullUnmarked; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.annotation.Collation; @@ -119,8 +120,20 @@ CodeBlock build() { String scrollPositionParameterName = context.getScrollPositionParameterName(); - builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(), - scrollPositionParameterName); + if (scrollPositionParameterName != null) { + + builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(), + scrollPositionParameterName); + } else { + String pageableParameterName = context.getPageableParameterName(); + if (pageableParameterName != null) { + builder.addStatement("return $L.matching($L).scroll($L.toScrollPosition())", + context.localVariable("finder"), query.name(), pageableParameterName); + } else { + builder.addStatement("return $L.matching($L).scroll($T.initial())", context.localVariable("finder"), + query.name(), ScrollPosition.class); + } + } } else { if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 50e191500a..c1d941036d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -15,10 +15,31 @@ */ package org.springframework.data.mongodb.core.aggregation; -import static org.springframework.data.domain.Sort.Direction.*; -import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static org.springframework.data.domain.Sort.Direction.ASC; +import static org.springframework.data.domain.Sort.Direction.DESC; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.DEFAULT_CONTEXT; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.bind; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.bucket; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.bucketAuto; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.count; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.facet; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.group; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.limit; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.lookup; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.match; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregationOptions; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.out; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.previousOperation; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.project; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.replaceRoot; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.sample; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.skip; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.sort; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.unwind; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatIllegalArgumentException; import java.io.BufferedInputStream; import java.text.ParseException; @@ -46,7 +67,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.core.io.ClassPathResource; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Sort; @@ -554,7 +574,7 @@ void findStatesWithPopulationOver10MillionAggregationExample() { /* //complex mongodb aggregation framework example from https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state - + db.zipcodes.aggregate( { $group: { @@ -1579,7 +1599,8 @@ void shouldLookupPeopleCorrectlyWithPipeline() { createUsersWithReferencedPersons(); TypedAggregation agg = newAggregation(User.class, // - lookup().from("person").localField("_id").foreignField("firstname").pipeline(match(where("firstname").is("u1"))).as("linkedPerson"), // + lookup().from("person").localField("_id").foreignField("firstname").pipeline(match(where("firstname").is("u1"))) + .as("linkedPerson"), // sort(ASC, "id")); AggregationResults results = mongoTemplate.aggregate(agg, User.class, Document.class); @@ -1598,8 +1619,10 @@ void shouldLookupPeopleCorrectlyWithPipelineAndLet() { createUsersWithReferencedPersons(); TypedAggregation agg = newAggregation(User.class, // - lookup().from("person").localField("_id").foreignField("firstname").let(Let.ExpressionVariable.newVariable("the_id").forField("_id")).pipeline( - match(ctx -> new Document("$expr", new Document("$eq", List.of("$$the_id", "u1"))))).as("linkedPerson"), + lookup().from("person").localField("_id").foreignField("firstname") + .let(Let.ExpressionVariable.newVariable("the_id").forField("_id")) + .pipeline(match(ctx -> new Document("$expr", new Document("$eq", List.of("$$the_id", "u1"))))) + .as("linkedPerson"), sort(ASC, "id")); AggregationResults results = mongoTemplate.aggregate(agg, User.class, Document.class); @@ -1956,9 +1979,8 @@ void percentileShouldBeAppliedCorrectly() { mongoTemplate.insert(objectToSave); mongoTemplate.insert(objectToSave2); - Aggregation agg = Aggregation.newAggregation( - project().and(ArithmeticOperators.valueOf("x").percentile(0.9, 0.4).and("y").and("xField")) - .as("percentileValues")); + Aggregation agg = Aggregation.newAggregation(project() + .and(ArithmeticOperators.valueOf("x").percentile(0.9, 0.4).and("y").and("xField")).as("percentileValues")); AggregationResults result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); @@ -1979,8 +2001,7 @@ void medianShouldBeAppliedCorrectly() { mongoTemplate.insert(objectToSave2); Aggregation agg = Aggregation.newAggregation( - project().and(ArithmeticOperators.valueOf("x").median().and("y").and("xField")) - .as("medianValue")); + project().and(ArithmeticOperators.valueOf("x").median().and("y").and("xField")).as("medianValue")); AggregationResults result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); @@ -2086,7 +2107,8 @@ void considersMongoIdWithinTypedCollections() { mongoTemplate.save(widget); Criteria criteria = Criteria.where("users").elemMatch(Criteria.where("id").is("4ee921aca44fd11b3254e001")); - AggregationResults aggregate = mongoTemplate.aggregate(newAggregation(match(criteria)), Widget.class, Widget.class); + AggregationResults aggregate = mongoTemplate.aggregate(newAggregation(match(criteria)), Widget.class, + Widget.class); assertThat(aggregate.getMappedResults()).contains(widget); } @@ -2097,9 +2119,7 @@ void shouldHonorFieldAliasesForFieldReferencesUsingFieldExposingOperation() { Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build(); mongoTemplate.insert(Arrays.asList(item1, item2), Item.class); - TypedAggregation aggregation = newAggregation(Item.class, - match(where("itemId").is("1")), - unwind("tags"), + TypedAggregation aggregation = newAggregation(Item.class, match(where("itemId").is("1")), unwind("tags"), match(where("itemId").is("1").and("tags").is("c"))); AggregationResults results = mongoTemplate.aggregate(aggregation, Document.class); List mappedResults = results.getMappedResults(); @@ -2114,9 +2134,7 @@ void projectShouldResetContextToAvoidMappingFieldsAgainstANoLongerExistingTarget Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build(); mongoTemplate.insert(Arrays.asList(item1, item2), Item.class); - TypedAggregation aggregation = newAggregation(Item.class, - match(where("itemId").is("1")), - unwind("tags"), + TypedAggregation aggregation = newAggregation(Item.class, match(where("itemId").is("1")), unwind("tags"), project().and("itemId").as("itemId").and("tags").as("tags"), match(where("itemId").is("1").and("tags").is("c"))); @@ -3097,4 +3115,29 @@ public String toString() { return "AggregationTests.UserRef(id=" + this.getId() + ", name=" + this.getName() + ")"; } } + + @Test + void xxx() { + + MyOhMy source = new MyOhMy(); + source.id = "id-1"; + source.firstname = "iwi"; + source.lastname = "wang"; + + mongoTemplate.save(source); + + TypedAggregation agg = newAggregation(MyOhMy.class, project("firstname")); + AggregationResults aggregate = mongoTemplate.aggregate(agg, MyMyOh.class); + assertThat(aggregate.getMappedResults()).hasOnlyElementsOfType(MyMyOh.class); + } + + static class MyOhMy { + @Id String id; + String firstname; + String lastname; + } + + interface MyMyOh { + String getFirstname(); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index e41c758dcb..8c60a3533e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -127,7 +127,6 @@ void appliesLimitToScrollingCorrectly() { } @Test // GH-4308 - @Disabled void appliesScrollPositionWithProjectionCorrectly() { Window page = repository.findCursorProjectionByLastnameLike(Pattern.compile(".*a.*"), @@ -163,12 +162,6 @@ void caseInSensitiveInClauseQuotesExpressions() { super.caseInSensitiveInClauseQuotesExpressions(); } - @Test // GH-4839 - @Disabled - void annotatedAggregationWithAggregationResultAsClosedInterfaceProjection() { - super.annotatedAggregationWithAggregationResultAsClosedInterfaceProjection(); - } - @Test // DATAMONGO-1608 @Disabled void findByFirstNameIgnoreCaseWithNull() { From 3e6c32d67f680753cd6ec4c08508b5b00c242455 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 25 Jul 2025 08:39:46 +0200 Subject: [PATCH 06/12] not pretty but works --- .../repository/aot/AotPlaceholders.java | 15 ++-- .../repository/aot/AotQueryCreator.java | 55 ++++++++++-- .../repository/aot/AotStringQuery.java | 25 +++++- .../MongoAotRepositoryFragmentSupport.java | 22 +++++ .../mongodb/repository/aot/QueryBlocks.java | 38 +++++--- .../repository/query/MongoQueryCreator.java | 1 - .../AotPersonRepositoryIntegrationTests.java | 89 ------------------- 7 files changed, 131 insertions(+), 114 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index 84bc0b4d46..a80db2401b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -19,6 +19,7 @@ import java.util.regex.Pattern; import kotlin.text.Regex; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -108,8 +109,8 @@ public static Shape polygon(int index) { return new PolygonPlaceholder(index); } - public static RegexPlaceholder regex(int index) { - return new RegexPlaceholder(index); + public static RegexPlaceholder regex(int index, @Nullable String options) { + return new RegexPlaceholder(index, options); } /** @@ -121,7 +122,6 @@ public static RegexPlaceholder regex(int index) { public interface Placeholder { String getValue(); - } /** @@ -274,9 +274,14 @@ public String toString() { static class RegexPlaceholder implements Placeholder { private final int index; - - public RegexPlaceholder(int index) { + private final String options; + public RegexPlaceholder(int index, String options) { this.index = index; + this.options = options; + } + + public @Nullable String regexOptions() { + return options != null ? "\"%s\"".formatted(options) : null; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index 2cb97f4fc1..b0c998e652 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -56,6 +56,8 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; +import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.TypeInformation; import org.springframework.util.ClassUtils; @@ -82,14 +84,14 @@ AotStringQuery createQuery(PartTree partTree, QueryMethod queryMethod, Method so ? mqm.isSearchQuery() || source.isAnnotationPresent(VectorSearch.class) : source.isAnnotationPresent(VectorSearch.class); - Query query = new AotMongoQueryCreator(partTree, - new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(queryMethod)), mappingContext, - geoNear, searchQuery).createQuery(); + PlaceholderParameterAccessor placeholderAccessor = new PlaceholderParameterAccessor(partTree, queryMethod); + Query query = new AotMongoQueryCreator(partTree, new PlaceholderConvertingParameterAccessor(placeholderAccessor), + mappingContext, geoNear, searchQuery).createQuery(); if (partTree.isLimiting()) { query.limit(partTree.getMaxResults()); } - return new AotStringQuery(query); + return new AotStringQuery(query, placeholderAccessor.getPlaceholders()); } static class AotMongoQueryCreator extends MongoQueryCreator { @@ -123,6 +125,14 @@ protected Criteria exists(Criteria criteria, Object param) { protected Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria, Object param) { + if (part.getType().equals(Type.LIKE)) { + return criteria.is(param); + } + + if(part.getType().equals(Type.NOT_LIKE)) { + return criteria.raw("$not", param); + } + if (param instanceof RegexPlaceholder) { return criteria.raw("$regex", param); } @@ -169,12 +179,34 @@ static class PlaceholderParameterAccessor implements MongoParameterAccessor { private final List placeholders; - public PlaceholderParameterAccessor(QueryMethod queryMethod) { + @Nullable Part getPartForIndex(PartTree partTree, Parameter parameter) { + if(!parameter.isBindable()) { + return null; + } + + List parts = partTree.getParts().stream().toList(); + int counter = 0; + for (Part part : parts) { + if(counter == parameter.getIndex()) { + return part; + } + counter += part.getNumberOfArguments(); + } + return null; + } + + public PlaceholderParameterAccessor(PartTree partTree, QueryMethod queryMethod) { + + if (queryMethod.getParameters().getNumberOfParameters() == 0) { placeholders = List.of(); } else { + + placeholders = new ArrayList<>(); Parameters parameters = queryMethod.getParameters(); + + for (Parameter parameter : parameters.toList()) { if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) { placeholders.add(parameter.getIndex(), AotPlaceholders.geoJson(parameter.getIndex(), "")); @@ -189,9 +221,14 @@ public PlaceholderParameterAccessor(QueryMethod queryMethod) { } else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) { placeholders.add(parameter.getIndex(), AotPlaceholders.polygon(parameter.getIndex())); } else if (ClassUtils.isAssignable(Pattern.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.regex(parameter.getIndex())); + placeholders.add(parameter.getIndex(), AotPlaceholders.regex(parameter.getIndex(), null)); } else { - placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); + Part partForIndex = getPartForIndex(partTree, parameter); + if(partForIndex != null && (partForIndex.getType().equals(Type.LIKE) || partForIndex.getType().equals(Type.NOT_LIKE))) { + placeholders.add(parameter.getIndex(), AotPlaceholders.regex(parameter.getIndex(), partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS) || partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.WHEN_POSSIBLE) ? "i": null)); + } else { + placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); + } } } } @@ -278,6 +315,10 @@ public boolean hasBindableNullValue() { public Iterator iterator() { return ((List) placeholders).iterator(); } + + public List getPlaceholders() { + return placeholders; + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java index 01373c2fb0..2f2bdf9a80 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.repository.aot; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -26,6 +28,8 @@ import org.springframework.data.mongodb.core.query.Field; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.aot.AotPlaceholders.Placeholder; +import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder; import org.springframework.util.StringUtils; import com.mongodb.ReadConcern; @@ -44,8 +48,11 @@ class AotStringQuery extends Query { private @Nullable String sort; private @Nullable String fields; - public AotStringQuery(Query query) { + private List placeholders = new ArrayList<>(); + + public AotStringQuery(Query query, List placeholders) { this.delegate = query; + this.placeholders = placeholders; } public AotStringQuery(String query) { @@ -72,6 +79,22 @@ public Query sort(String sort) { return this; } + boolean isRegexPlaceholderAt(int index) { + if(this.placeholders.isEmpty()) { + return false; + } + + return this.placeholders.get(index) instanceof RegexPlaceholder; + } + + String getRegexOptions(int index) { + if(this.placeholders.isEmpty()) { + return null; + } + + return this.placeholders.get(index) instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null; + } + @Override public Field fields() { return delegate.fields(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index 055e8e0742..4dcb73aa8c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -17,11 +17,14 @@ import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Consumer; +import java.util.regex.Pattern; +import org.bson.BsonRegularExpression; import org.bson.Document; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; @@ -44,6 +47,8 @@ import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.MongoRegexCreator; +import org.springframework.data.mongodb.core.query.MongoRegexCreator.MatchMode; import org.springframework.data.mongodb.repository.query.MongoParameters; import org.springframework.data.mongodb.repository.query.MongoParametersParameterAccessor; import org.springframework.data.mongodb.util.json.ParameterBindingContext; @@ -224,6 +229,23 @@ protected Collation collationOf(@Nullable Object source) { "Unsupported collation source [%s]".formatted(ObjectUtils.nullSafeClassName(source))); } + protected Object likeExpression(Object source, String options) { + + if (source instanceof String sv) { + return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, MatchMode.LIKE), options); + } + if (source instanceof Pattern pattern) { + return pattern; + } + if (source instanceof Collection collection) { + return collection.stream().map(it -> likeExpression(it, options)).toList(); + } + if (ObjectUtils.isArray(source)) { + return likeExpression(List.of(source), options); + } + return source; + } + protected BasicQuery createQuery(Method method, String queryString, Object... parameters) { Document queryDocument = bindParameters(method, queryString, parameters); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index e79cf3fdae..84e62b9c44 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.repository.aot; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.bson.Document; @@ -31,6 +33,7 @@ import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryMethod; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; +import org.springframework.data.util.Lazy; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; @@ -156,7 +159,7 @@ static class QueryCodeBlockBuilder { private final AotQueryMethodGenerationContext context; private final MongoQueryMethod queryMethod; - private final String parameterNames; + private final Lazy parameterNames; private QueryInteraction source; private String queryVariableName; @@ -166,13 +169,26 @@ static class QueryCodeBlockBuilder { this.context = context; this.queryMethod = queryMethod; - String parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", "); + this.parameterNames = Lazy.of(() -> { + List allParameterNames = context.getAllParameterNames(); + List formatted = new ArrayList<>(allParameterNames.size()); + for (int i = 0; i < allParameterNames.size(); i++) { - if (StringUtils.hasText(parameterNames)) { - this.parameterNames = ", " + parameterNames; - } else { - this.parameterNames = ""; - } + String parameterName = allParameterNames.get(i); + if (source.getQuery().isRegexPlaceholderAt(i) && context.getMethodParameter(parameterName).getParameterType() == String.class) { + parameterName = "%s(%s, %s)".formatted("likeExpression", parameterName, source.getQuery().getRegexOptions(i)); + } + formatted.add(parameterName); + + } + String parameterNames = StringUtils.collectionToDelimitedString(formatted, ", "); + + if (StringUtils.hasText(parameterNames)) { + return ", " + parameterNames; + } else { + return ""; + } + }); } QueryCodeBlockBuilder filter(QueryInteraction query) { @@ -196,14 +212,14 @@ CodeBlock build() { if (StringUtils.hasText(source.getQuery().getFieldsString())) { VariableSnippet fields = Snippet.declare(builder).variable(Document.class, context.localVariable("fields")) - .of(MongoCodeBlocks.asDocument(source.getQuery().getFieldsString(), parameterNames)); + .of(MongoCodeBlocks.asDocument(source.getQuery().getFieldsString(), parameterNames.get())); builder.addStatement("$L.setFieldsObject($L)", queryVariableName, fields.getVariableName()); } if (StringUtils.hasText(source.getQuery().getSortString())) { VariableSnippet sort = Snippet.declare(builder).variable(Document.class, context.localVariable("sort")) - .of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), parameterNames)); + .of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), parameterNames.get())); builder.addStatement("$L.setSortObject($L)", queryVariableName, sort.getVariableName()); } @@ -264,7 +280,7 @@ CodeBlock build() { builder.addStatement( "$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S$L)))", - queryVariableName, collationString, parameterNames); + queryVariableName, collationString, parameterNames.get()); } } } @@ -288,7 +304,7 @@ private CodeBlock renderExpressionToQuery() { return CodeBlock.of("new $T(new $T())", BasicQuery.class, Document.class); } else if (MongoCodeBlocks.containsPlaceholder(source)) { Builder builder = CodeBlock.builder(); - builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S$L)", source, parameterNames); + builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S$L)", source, parameterNames.get()); return builder.build(); } else { return CodeBlock.of("new $T(parse($S))", BasicQuery.class, source); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 84d424458b..ced7040f4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -425,7 +425,6 @@ private java.util.List valueAsList(Object value, Part part) { streamable = streamable.map(it -> { if (it instanceof String sv) { - return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions); } return it; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index 8c60a3533e..6763aaa8c2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -47,8 +47,6 @@ * @author Mark Paluch */ @ContextConfiguration(classes = AotPersonRepositoryIntegrationTests.Config.class) -// @Disabled("Several mismatches, some class-loader visibility issues and some behavioral differences remain to be -// fixed") class AotPersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { @Configuration @@ -75,81 +73,6 @@ PersonRepository personRepository(MongoOperations mongoOperations, ApplicationCo } - @Test // GH-4397 - @Override - void executesFinderCorrectlyWithSortAndLimit() { - - List page = repository.findByLastnameLike(Pattern.compile(".*a.*"), - Sort.by(Direction.ASC, "lastname", "firstname"), Limit.of(2)); - - assertThat(page).containsExactly(carter, stefan); - } - - @Test - @Override - void findsPersonsByFirstnameLike() { - - List result = repository.findByFirstnameLike(Pattern.compile("Bo.*")); - assertThat(result).hasSize(1).contains(boyd); - } - - @Test // DATAMONGO-1424 - @Override - void findsPersonsByFirstnameNotLike() { - - List result = repository.findByFirstnameNotLike(Pattern.compile("Bo.*")); - assertThat(result).hasSize((int) (repository.count() - 1)); - assertThat(result).doesNotContain(boyd); - } - - @Test // GH-4308 - @Override - void appliesScrollPositionCorrectly() { - - Window page = repository.findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern.compile(".*a.*"), - ScrollPosition.keyset()); - - assertThat(page.isLast()).isFalse(); - assertThat(page.size()).isEqualTo(2); - assertThat(page).contains(carter); - } - - @Test // GH-4397 - @Override - void appliesLimitToScrollingCorrectly() { - - Window page = repository.findByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern.compile(".*a.*"), - ScrollPosition.keyset(), Limit.of(2)); - - assertThat(page.isLast()).isFalse(); - assertThat(page.size()).isEqualTo(2); - assertThat(page).contains(carter); - } - - @Test // GH-4308 - void appliesScrollPositionWithProjectionCorrectly() { - - Window page = repository.findCursorProjectionByLastnameLike(Pattern.compile(".*a.*"), - PageRequest.of(0, 2, Sort.by(Direction.ASC, "lastname", "firstname"))); - - assertThat(page.isLast()).isFalse(); - assertThat(page.size()).isEqualTo(2); - - assertThat(page).element(0).isEqualTo(new PersonSummaryDto(carter.getFirstname(), carter.getLastname())); - } - - @Test // DATADOC-236 - @Override - void appliesStaticAndDynamicSorting() { - List result = repository.findByFirstnameLikeOrderByLastnameAsc(Pattern.compile(".*e.*"), Sort.by("age")); - assertThat(result).hasSize(5); - assertThat(result.get(0)).isEqualTo(carter); - assertThat(result.get(1)).isEqualTo(stefan); - assertThat(result.get(2)).isEqualTo(oliver); - assertThat(result.get(3)).isEqualTo(dave); - assertThat(result.get(4)).isEqualTo(leroi); - } - @Test // DATAMONGO-1608 @Disabled void findByFirstnameLikeWithNull() { @@ -181,18 +104,6 @@ void resultProjectionWithOptionalIsExecutedCorrectly() { super.resultProjectionWithOptionalIsExecutedCorrectly(); } - @Test - @Override - void executesPagedFinderCorrectly() { - - Page page = repository.findByLastnameLike(Pattern.compile(".*a.*"), - PageRequest.of(0, 2, Direction.ASC, "lastname", "firstname")); - assertThat(page.isFirst()).isTrue(); - assertThat(page.isLast()).isFalse(); - assertThat(page.getNumberOfElements()).isEqualTo(2); - assertThat(page).contains(carter, stefan); - } - @Test // DATAMONGO-990 @Disabled void shouldFindByFirstnameAndCurrentUserWithCustomQuery() { From 75d1984b3b10702eaada078c1f0602fda69e083b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 29 Jul 2025 09:18:01 +0200 Subject: [PATCH 07/12] wrap single array parameter to avoid errors passing it on as multiple ones. Also fix projection field name mapping error --- .../data/mongodb/core/QueryOperations.java | 3 +++ .../mongodb/repository/aot/QueryBlocks.java | 13 +++++++++++- .../AotPersonRepositoryIntegrationTests.java | 20 ------------------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index 602713cb77..24e2c99a63 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -377,6 +377,9 @@ Document getMappedFields(@Nullable MongoPersistentEntity entity, EntityProjec mappedFields = queryMapper.getMappedFields(fields, entity); } else { mappedFields = propertyOperations.computeMappedFieldsForProjection(projection, fields); + if(projection.getMappedType().getType().isInterface()) { + mappedFields = queryMapper.getMappedFields(mappedFields, entity); + } mappedFields = queryMapper.addMetaAttributes(mappedFields, entity); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index 84e62b9c44..56092bd109 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -172,18 +172,29 @@ static class QueryCodeBlockBuilder { this.parameterNames = Lazy.of(() -> { List allParameterNames = context.getAllParameterNames(); List formatted = new ArrayList<>(allParameterNames.size()); + boolean containsArrayParameter = false; for (int i = 0; i < allParameterNames.size(); i++) { String parameterName = allParameterNames.get(i); - if (source.getQuery().isRegexPlaceholderAt(i) && context.getMethodParameter(parameterName).getParameterType() == String.class) { + Class parameterType = context.getMethodParameter(parameterName).getParameterType(); + if (source.getQuery().isRegexPlaceholderAt(i) && parameterType == String.class) { parameterName = "%s(%s, %s)".formatted("likeExpression", parameterName, source.getQuery().getRegexOptions(i)); } formatted.add(parameterName); + + if(parameterType != null && parameterType.isArray()) { + containsArrayParameter = true; + } } + + String parameterNames = StringUtils.collectionToDelimitedString(formatted, ", "); if (StringUtils.hasText(parameterNames)) { + if(containsArrayParameter && formatted.size() == 1) { + return ", new java.lang.Object[] {" + parameterNames + "}"; + } return ", " + parameterNames; } else { return ""; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index 6763aaa8c2..102072879f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -15,29 +15,16 @@ */ package org.springframework.data.mongodb.repository; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; -import java.util.regex.Pattern; - import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; -import org.springframework.data.domain.Limit; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.TestMongoConfiguration; import org.springframework.data.mongodb.repository.aot.AotFragmentTestConfigurationSupport; import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; -import org.springframework.data.mongodb.test.util.DirtiesStateExtension.DirtiesState; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.test.context.ContextConfiguration; @@ -97,13 +84,6 @@ void caseSensitiveInClauseIgnoresExpressions() { super.caseSensitiveInClauseIgnoresExpressions(); } - @Test // GH-3656 - @DirtiesState - @Disabled - void resultProjectionWithOptionalIsExecutedCorrectly() { - super.resultProjectionWithOptionalIsExecutedCorrectly(); - } - @Test // DATAMONGO-990 @Disabled void shouldFindByFirstnameAndCurrentUserWithCustomQuery() { From 30a69fb59b9f11d8804632bab03e811a640976dc Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 29 Jul 2025 09:47:18 +0200 Subject: [PATCH 08/12] Allow pattern type for like queries --- .../aot/MongoAotRepositoryFragmentSupport.java | 10 +++++++--- .../data/mongodb/repository/aot/QueryBlocks.java | 8 ++++++-- .../repository/query/MongoQueryCreator.java | 4 ++++ .../AbstractPersonRepositoryIntegrationTests.java | 15 +++++++++++++++ .../antora/modules/ROOT/pages/mongodb/aot.adoc | 12 +++++++++++- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index 4dcb73aa8c..d8fb5428b9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -229,7 +229,11 @@ protected Collation collationOf(@Nullable Object source) { "Unsupported collation source [%s]".formatted(ObjectUtils.nullSafeClassName(source))); } - protected Object likeExpression(Object source, String options) { + protected Object toRegex(Object source) { + return toRegex(source, null); + } + + protected Object toRegex(Object source, String options) { if (source instanceof String sv) { return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, MatchMode.LIKE), options); @@ -238,10 +242,10 @@ protected Object likeExpression(Object source, String options) { return pattern; } if (source instanceof Collection collection) { - return collection.stream().map(it -> likeExpression(it, options)).toList(); + return collection.stream().map(it -> toRegex(it, options)).toList(); } if (ObjectUtils.isArray(source)) { - return likeExpression(List.of(source), options); + return toRegex(List.of(source), options); } return source; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index 56092bd109..b028e11ec9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -178,11 +178,15 @@ static class QueryCodeBlockBuilder { String parameterName = allParameterNames.get(i); Class parameterType = context.getMethodParameter(parameterName).getParameterType(); if (source.getQuery().isRegexPlaceholderAt(i) && parameterType == String.class) { - parameterName = "%s(%s, %s)".formatted("likeExpression", parameterName, source.getQuery().getRegexOptions(i)); + String regexOptions = source.getQuery().getRegexOptions(i); + if(StringUtils.hasText(regexOptions)) { + parameterName = "%s(%s)".formatted("toRegex", parameterName); + } else { + parameterName = "%s(%s, %s)".formatted("toRegex", parameterName, regexOptions); + } } formatted.add(parameterName); - if(parameterType != null && parameterType.isArray()) { containsArrayParameter = true; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index ced7040f4f..9d14f4b997 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -374,6 +374,10 @@ private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, @Nullab "Argument for creating $regex pattern for property '%s' must not be null", part.getProperty().getSegment())); } + if(value instanceof Pattern pattern) { + return criteria.regex(pattern); + } + return criteria.regex(toLikeRegex(value.toString(), part), toRegexOptions(part)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index bacd4b760c..c361d5513d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -198,6 +198,13 @@ void findsPersonsByFirstnameLike() { assertThat(result).hasSize(1).contains(boyd); } + @Test // GH-5027 + void findsPersonsByFirstnameLikeWithPattern() { + + List result = repository.findByFirstnameLike(Pattern.compile("Bo.*")); + assertThat(result).hasSize(1).contains(boyd); + } + @Test // DATAMONGO-1608 void findByFirstnameLikeWithNull() { assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameLike((String)null)); @@ -1302,6 +1309,14 @@ void findsPersonsByFirstnameNotLike() { assertThat(result).doesNotContain(boyd); } + @Test // GH-5027 + void findsPersonsByFirstnameNotLikeWithPattern() { + + List result = repository.findByFirstnameNotLike(Pattern.compile("Bo.*")); + assertThat(result).hasSize((int) (repository.count() - 1)); + assertThat(result).doesNotContain(boyd); + } + @Test // DATAMONGO-1539 void countsPersonsByFirstname() { assertThat(repository.countByThePersonsFirstname("Dave")).isEqualTo(1L); diff --git a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc index 0c496c90f0..991581c8e2 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc @@ -72,15 +72,25 @@ These are typically all query methods that are not backed by an xref:repositorie * Methods annotated with `@Aggregation`, `@Update`, and `@VectorSearch` * `@Hint`, `@Meta`, and `@ReadPreference` support * `Page`, `Slice`, and `Optional` return types -* DTO Projections +* DTO & Interface Projections **Limitations** * `@Meta.allowDiskUse` and `flags` are not evaluated. * Limited `Collation` detection. +* No support for in-clauses with pattern matching / case insensitivity **Excluded methods** * `CrudRepository` and other base interface methods * Querydsl and Query by Example methods * Query Methods obtaining MQL from a file + +[TIP] +==== +Consider using `Pattern` instead of `String` as parameter type when working with derived queries using the `LIKE` keyword. +[source,java] +---- +List findByLastnameLike(Pattern lastname); +---- +==== From da88469cf45a4771c197248670b0c4625ba891ef Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 29 Jul 2025 11:14:49 +0200 Subject: [PATCH 09/12] Fix AOT Repo integration test setup. Make sure to register evaluation context with extension lookup. --- .../AotPersonRepositoryIntegrationTests.java | 6 ------ .../aot/AotFragmentTestConfigurationSupport.java | 13 +++++++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index 102072879f..bac2bde266 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -84,12 +84,6 @@ void caseSensitiveInClauseIgnoresExpressions() { super.caseSensitiveInClauseIgnoresExpressions(); } - @Test // DATAMONGO-990 - @Disabled - void shouldFindByFirstnameAndCurrentUserWithCustomQuery() { - super.shouldFindByFirstnameAndCurrentUserWithCustomQuery(); - } - @Test // GH-3395, GH-4404 @Disabled void caseInSensitiveInClause() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java index f2feeac4ba..ef44c2da8a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java @@ -27,13 +27,17 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.env.StandardEnvironment; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.util.Lazy; import org.springframework.util.ReflectionUtils; /** @@ -124,6 +128,8 @@ private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( return new RepositoryFactoryBeanSupport.FragmentCreationContext() { + final Lazy projectionFactory = Lazy.of(SpelAwareProxyProjectionFactory::new); + @Override public RepositoryMetadata getRepositoryMetadata() { return repositoryContext.getRepositoryInformation(); @@ -131,12 +137,15 @@ public RepositoryMetadata getRepositoryMetadata() { @Override public ValueExpressionDelegate getValueExpressionDelegate() { - return ValueExpressionDelegate.create(); + + QueryMethodValueEvaluationContextAccessor queryMethodValueEvaluationContextAccessor = new QueryMethodValueEvaluationContextAccessor( + new StandardEnvironment(), repositoryContext.getBeanFactory()); + return new ValueExpressionDelegate(queryMethodValueEvaluationContextAccessor, ValueExpressionParser.create()); } @Override public ProjectionFactory getProjectionFactory() { - return new SpelAwareProxyProjectionFactory(); + return projectionFactory.get(); } }; } From d0a9b73c446fdf3dae6d890a79e91894f5f88cc5 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 29 Jul 2025 14:12:16 +0200 Subject: [PATCH 10/12] Refactor parameter names handling --- .../repository/aot/AggregationBlocks.java | 8 +- .../repository/aot/AotPlaceholders.java | 5 +- .../repository/aot/MongoCodeBlocks.java | 14 ++- .../mongodb/repository/aot/QueryBlocks.java | 93 ++++++++++++------- 4 files changed, 70 insertions(+), 50 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index 3b18bfe84f..86d3e4ddcb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -178,13 +178,7 @@ static class AggregationCodeBlockBuilder { this.context = context; this.queryMethod = queryMethod; - String parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", "); - - if (StringUtils.hasText(parameterNames)) { - this.parameterNames = ", " + parameterNames; - } else { - this.parameterNames = ""; - } + this.parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", "); } AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index a80db2401b..97827ad3c7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -16,9 +16,7 @@ package org.springframework.data.mongodb.repository.aot; import java.util.List; -import java.util.regex.Pattern; -import kotlin.text.Regex; import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; @@ -275,13 +273,14 @@ static class RegexPlaceholder implements Placeholder { private final int index; private final String options; + public RegexPlaceholder(int index, String options) { this.index = index; this.options = options; } public @Nullable String regexOptions() { - return options != null ? "\"%s\"".formatted(options) : null; + return options; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java index c1261ab055..81bd096ab5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -19,7 +19,6 @@ import org.bson.Document; import org.jspecify.annotations.Nullable; - import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.mongodb.repository.ReadPreference; import org.springframework.data.mongodb.repository.aot.AggregationBlocks.AggregationCodeBlockBuilder; @@ -164,20 +163,27 @@ static GeoNearExecutionCodeBlockBuilder geoNearExecutionBlockBuilder(AotQueryMet } static CodeBlock asDocument(String source, String argNames) { + return asDocument(source, CodeBlock.of("$L", argNames)); + } + + static CodeBlock asDocument(String source, CodeBlock arguments) { Builder builder = CodeBlock.builder(); if (!StringUtils.hasText(source)) { builder.add("new $T()", Document.class); } else if (containsPlaceholder(source)) { - builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S$L)", source, argNames); + if (arguments.isEmpty()) { + builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S)", source); + } else { + builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", source, arguments); + } } else { builder.add("parse($S)", source); } return builder.build(); } - static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, - String argNames) { + static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, String argNames) { Builder builder = CodeBlock.builder(); if (!StringUtils.hasText(source)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index b028e11ec9..41c45e1af4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.repository.aot; -import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -159,7 +158,7 @@ static class QueryCodeBlockBuilder { private final AotQueryMethodGenerationContext context; private final MongoQueryMethod queryMethod; - private final Lazy parameterNames; + private final Lazy queryParameters; private QueryInteraction source; private String queryVariableName; @@ -168,42 +167,54 @@ static class QueryCodeBlockBuilder { this.context = context; this.queryMethod = queryMethod; + this.queryParameters = Lazy.of(this::queryParametersCodeBlock); + } - this.parameterNames = Lazy.of(() -> { - List allParameterNames = context.getAllParameterNames(); - List formatted = new ArrayList<>(allParameterNames.size()); - boolean containsArrayParameter = false; - for (int i = 0; i < allParameterNames.size(); i++) { - - String parameterName = allParameterNames.get(i); - Class parameterType = context.getMethodParameter(parameterName).getParameterType(); - if (source.getQuery().isRegexPlaceholderAt(i) && parameterType == String.class) { - String regexOptions = source.getQuery().getRegexOptions(i); - if(StringUtils.hasText(regexOptions)) { - parameterName = "%s(%s)".formatted("toRegex", parameterName); - } else { - parameterName = "%s(%s, %s)".formatted("toRegex", parameterName, regexOptions); - } - } - formatted.add(parameterName); + CodeBlock queryParametersCodeBlock() { - if(parameterType != null && parameterType.isArray()) { - containsArrayParameter = true; - } - } + List allParameterNames = context.getAllParameterNames(); + if (allParameterNames.isEmpty()) { + return CodeBlock.builder().build(); + } + + CodeBlock.Builder formatted = CodeBlock.builder(); + boolean containsArrayParameter = false; + for (int i = 0; i < allParameterNames.size(); i++) { - String parameterNames = StringUtils.collectionToDelimitedString(formatted, ", "); + String parameterName = allParameterNames.get(i); + Class parameterType = context.getMethodParameter(parameterName).getParameterType(); + if (source.getQuery().isRegexPlaceholderAt(i) && parameterType == String.class) { + String regexOptions = source.getQuery().getRegexOptions(i); - if (StringUtils.hasText(parameterNames)) { - if(containsArrayParameter && formatted.size() == 1) { - return ", new java.lang.Object[] {" + parameterNames + "}"; + if (StringUtils.hasText(regexOptions)) { + formatted.add(CodeBlock.of("toRegex($L)", parameterName)); + } else { + formatted.add(CodeBlock.of("toRegex($L, $S)", parameterName, regexOptions)); } - return ", " + parameterNames; } else { - return ""; + formatted.add(CodeBlock.of("$L", parameterName)); } - }); + + if (i + 1 < allParameterNames.size()) { + formatted.add(", "); + } + + if (!containsArrayParameter && parameterType != null && parameterType.isArray()) { + containsArrayParameter = true; + } + } + + // wrap single array argument to avoid problems with vargs when calling method + if (containsArrayParameter && allParameterNames.size() == 1) { + return CodeBlock.of("new $T[] { $L }", Object.class, formatted.build()); + } + + return formatted.build(); + } + + public CodeBlock getQueryParameters() { + return queryParameters.get(); } QueryCodeBlockBuilder filter(QueryInteraction query) { @@ -227,14 +238,14 @@ CodeBlock build() { if (StringUtils.hasText(source.getQuery().getFieldsString())) { VariableSnippet fields = Snippet.declare(builder).variable(Document.class, context.localVariable("fields")) - .of(MongoCodeBlocks.asDocument(source.getQuery().getFieldsString(), parameterNames.get())); + .of(MongoCodeBlocks.asDocument(source.getQuery().getFieldsString(), queryParameters.get())); builder.addStatement("$L.setFieldsObject($L)", queryVariableName, fields.getVariableName()); } if (StringUtils.hasText(source.getQuery().getSortString())) { VariableSnippet sort = Snippet.declare(builder).variable(Document.class, context.localVariable("sort")) - .of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), parameterNames.get())); + .of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), getQueryParameters())); builder.addStatement("$L.setSortObject($L)", queryVariableName, sort.getVariableName()); } @@ -293,9 +304,15 @@ CodeBlock build() { org.springframework.data.mongodb.core.query.Collation.class, collationString); } else { - builder.addStatement( - "$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S$L)))", - queryVariableName, collationString, parameterNames.get()); + if (getQueryParameters().isEmpty()) { + builder.addStatement( + "$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S)))", + queryVariableName, collationString); + } else { + builder.addStatement( + "$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S, $L)))", + queryVariableName, collationString, getQueryParameters()); + } } } } @@ -319,7 +336,11 @@ private CodeBlock renderExpressionToQuery() { return CodeBlock.of("new $T(new $T())", BasicQuery.class, Document.class); } else if (MongoCodeBlocks.containsPlaceholder(source)) { Builder builder = CodeBlock.builder(); - builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S$L)", source, parameterNames.get()); + if (getQueryParameters().isEmpty()) { + builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S)", source); + } else { + builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", source, getQueryParameters()); + } return builder.build(); } else { return CodeBlock.of("new $T(parse($S))", BasicQuery.class, source); From c84a6c2e4c09e033dc7635fefdb2e70d4ee5d106 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 30 Jul 2025 13:36:18 +0200 Subject: [PATCH 11/12] Use data commons expression marker --- .../repository/aot/AggregationBlocks.java | 2 +- .../mongodb/repository/aot/MongoCodeBlocks.java | 13 +++++++------ .../aot/MongoRepositoryContributor.java | 5 +---- .../data/mongodb/repository/aot/QueryBlocks.java | 16 ++++++++-------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index 86d3e4ddcb..57687d7e12 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -304,7 +304,7 @@ private CodeBlock aggregationStages(String stageListVariableName, Collection } Builder builder = CodeBlock.builder(); - builder.add("($T) evaluate(ExpressionMarker.class.getEnclosingMethod(), $S$L)", targetType, value, + builder.add("($T) evaluate($L, $S$L)", targetType, context.getExpressionMarker().enclosingMethod(), value, parameterNames); return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index a4a8422a78..e3ce6ad6ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -215,7 +215,7 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB CodeBlock.Builder builder = CodeBlock.builder(); - builder.addStatement("class ExpressionMarker{}"); +// builder.addStatement("class ExpressionMarker{}"); builder.add(finalContribution.contribute(context)); return builder.build(); @@ -240,9 +240,6 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor AotStringQuery aotStringQuery = queryCreator.createQuery(partTree, queryMethod, source); query = new QueryInteraction(aotStringQuery, partTree.isCountProjection(), partTree.isDelete(), partTree.isExistsProjection()); -// if(partTree.isLimiting()) { -// query.s -// } } if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index 41c45e1af4..391e6cc07f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -238,14 +238,14 @@ CodeBlock build() { if (StringUtils.hasText(source.getQuery().getFieldsString())) { VariableSnippet fields = Snippet.declare(builder).variable(Document.class, context.localVariable("fields")) - .of(MongoCodeBlocks.asDocument(source.getQuery().getFieldsString(), queryParameters.get())); + .of(MongoCodeBlocks.asDocument(context.getExpressionMarker(), source.getQuery().getFieldsString(), queryParameters.get())); builder.addStatement("$L.setFieldsObject($L)", queryVariableName, fields.getVariableName()); } if (StringUtils.hasText(source.getQuery().getSortString())) { VariableSnippet sort = Snippet.declare(builder).variable(Document.class, context.localVariable("sort")) - .of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), getQueryParameters())); + .of(MongoCodeBlocks.asDocument(context.getExpressionMarker(), source.getQuery().getSortString(), getQueryParameters())); builder.addStatement("$L.setSortObject($L)", queryVariableName, sort.getVariableName()); } @@ -306,12 +306,12 @@ CodeBlock build() { if (getQueryParameters().isEmpty()) { builder.addStatement( - "$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S)))", - queryVariableName, collationString); + "$L.collation(collationOf(evaluate($L, $S)))", + queryVariableName, context.getExpressionMarker().enclosingMethod(), collationString); } else { builder.addStatement( - "$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S, $L)))", - queryVariableName, collationString, getQueryParameters()); + "$L.collation(collationOf(evaluate($L, $S, $L)))", + queryVariableName, context.getExpressionMarker().enclosingMethod(), collationString, getQueryParameters()); } } } @@ -337,9 +337,9 @@ private CodeBlock renderExpressionToQuery() { } else if (MongoCodeBlocks.containsPlaceholder(source)) { Builder builder = CodeBlock.builder(); if (getQueryParameters().isEmpty()) { - builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S)", source); + builder.add("createQuery($L, $S)", context.getExpressionMarker().enclosingMethod(), source); } else { - builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", source, getQueryParameters()); + builder.add("createQuery($L, $S, $L)", context.getExpressionMarker().enclosingMethod(), source, getQueryParameters()); } return builder.build(); } else { From 9bdb6a427d927f9c1e7f1043b507b9128747fa9d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 30 Jul 2025 15:41:58 +0200 Subject: [PATCH 12/12] Fix nullable issues. --- .../repository/aot/AggregationBlocks.java | 2 +- .../repository/aot/AotPlaceholders.java | 32 +++++++++---------- .../repository/aot/AotStringQuery.java | 2 +- .../MongoAotRepositoryFragmentSupport.java | 3 +- .../aot/MongoRepositoryContributor.java | 1 + .../data/mongodb/repository/aot/Snippet.java | 6 ++++ .../repository/query/MongoQueryCreator.java | 5 +-- .../repository/query/MongoQueryExecution.java | 2 +- 8 files changed, 31 insertions(+), 22 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index 57687d7e12..c376fb11fd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -156,7 +156,7 @@ private static Class getOutputType(MongoQueryMethod queryMethod) { Class outputType = queryMethod.getReturnedObjectType(); if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) { outputType = Document.class; - } else if (ClassUtils.isAssignable(AggregationResults.class, outputType)) { + } else if (ClassUtils.isAssignable(AggregationResults.class, outputType) && queryMethod.getReturnType().getComponentType() != null) { outputType = queryMethod.getReturnType().getComponentType().getType(); } return outputType; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index 97827ad3c7..b6cbe50833 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -53,7 +53,7 @@ static Placeholder indexed(int position) { * @param type * @return */ - public static Shape geoJson(int index, String type) { + static Shape geoJson(int index, String type) { return new GeoJsonPlaceholder(index, type); } @@ -63,7 +63,7 @@ public static Shape geoJson(int index, String type) { * @param index zero-based index referring to the bindable method parameter. * @return */ - public static Point point(int index) { + static Point point(int index) { return new PointPlaceholder(index); } @@ -73,7 +73,7 @@ public static Point point(int index) { * @param index zero-based index referring to the bindable method parameter. * @return */ - public static Shape circle(int index) { + static Shape circle(int index) { return new CirclePlaceholder(index); } @@ -83,7 +83,7 @@ public static Shape circle(int index) { * @param index zero-based index referring to the bindable method parameter. * @return */ - public static Shape box(int index) { + static Shape box(int index) { return new BoxPlaceholder(index); } @@ -93,7 +93,7 @@ public static Shape box(int index) { * @param index zero-based index referring to the bindable method parameter. * @return */ - public static Shape sphere(int index) { + static Shape sphere(int index) { return new SpherePlaceholder(index); } @@ -103,11 +103,11 @@ public static Shape sphere(int index) { * @param index zero-based index referring to the bindable method parameter. * @return */ - public static Shape polygon(int index) { + static Shape polygon(int index) { return new PolygonPlaceholder(index); } - public static RegexPlaceholder regex(int index, @Nullable String options) { + static RegexPlaceholder regex(int index, @Nullable String options) { return new RegexPlaceholder(index, options); } @@ -117,7 +117,7 @@ public static RegexPlaceholder regex(int index, @Nullable String options) { * @since 5.0 * @author Christoph Strobl */ - public interface Placeholder { + interface Placeholder { String getValue(); } @@ -143,7 +143,7 @@ private static class PointPlaceholder extends Point implements Placeholder { private final int index; - public PointPlaceholder(int index) { + PointPlaceholder(int index) { super(Double.NaN, Double.NaN); this.index = index; } @@ -188,7 +188,7 @@ private static class CirclePlaceholder extends Circle implements Placeholder { private final int index; - public CirclePlaceholder(int index) { + CirclePlaceholder(int index) { super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); // this.index = index; } @@ -209,7 +209,7 @@ private static class BoxPlaceholder extends Box implements Placeholder { private final int index; - public BoxPlaceholder(int index) { + BoxPlaceholder(int index) { super(new PointPlaceholder(index), new PointPlaceholder(index)); this.index = index; } @@ -230,7 +230,7 @@ private static class SpherePlaceholder extends Sphere implements Placeholder { private final int index; - public SpherePlaceholder(int index) { + SpherePlaceholder(int index) { super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); // this.index = index; } @@ -251,7 +251,7 @@ private static class PolygonPlaceholder extends Polygon implements Placeholder { private final int index; - public PolygonPlaceholder(int index) { + PolygonPlaceholder(int index) { super(new PointPlaceholder(index), new PointPlaceholder(index), new PointPlaceholder(index), new PointPlaceholder(index)); this.index = index; @@ -272,14 +272,14 @@ public String toString() { static class RegexPlaceholder implements Placeholder { private final int index; - private final String options; + private final @Nullable String options; - public RegexPlaceholder(int index, String options) { + RegexPlaceholder(int index, @Nullable String options) { this.index = index; this.options = options; } - public @Nullable String regexOptions() { + @Nullable String regexOptions() { return options; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java index 2f2bdf9a80..c0920ba771 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java @@ -87,7 +87,7 @@ boolean isRegexPlaceholderAt(int index) { return this.placeholders.get(index) instanceof RegexPlaceholder; } - String getRegexOptions(int index) { + @Nullable String getRegexOptions(int index) { if(this.placeholders.isEmpty()) { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index d8fb5428b9..83c96e5494 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -233,7 +233,7 @@ protected Object toRegex(Object source) { return toRegex(source, null); } - protected Object toRegex(Object source, String options) { + protected Object toRegex(Object source, @Nullable String options) { if (source instanceof String sv) { return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, MatchMode.LIKE), options); @@ -256,6 +256,7 @@ protected BasicQuery createQuery(Method method, String queryString, Object... pa return new BasicQuery(queryDocument); } + @SuppressWarnings("NullAway") protected AggregationPipeline createPipeline(List rawStages) { if (rawStages.isEmpty()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index e3ce6ad6ed..0da74d06f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -93,6 +93,7 @@ public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { this.queryCreator = new AotQueryCreator(this.mappingContext); } + @SuppressWarnings("NullAway") private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource configSource, ClassLoader classLoader) { String location = configSource != null ? configSource.getNamedQueryLocation().orElse(null) : null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java index 2518be58b0..9fafdb1712 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java @@ -23,6 +23,7 @@ import org.springframework.data.mongodb.repository.aot.Snippet.BuilderStyleVariableBuilder.BuilderStyleVariableBuilderImpl; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.util.Assert; /** * @author Christoph Strobl @@ -192,12 +193,17 @@ public BuilderStyleMethodArgumentBuilder call(String methodName) { @Override public BuilderStyleBuilder with(Snippet snippet) { + + Assert.notNull(targetMethodName, "TargetMethodName must be set before calling this method"); + new BuilderStyleSnippet(targetVariableName, targetMethodName, snippet).appendTo(targetBuilder); return this; } @Override public VariableSnippet variable() { + + Assert.notNull(variableSnippet, "VariableSnippet must not be null"); return this.variableSnippet; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 9d14f4b997..807dc518d3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -258,8 +258,9 @@ private Criteria createNearCriteria(MongoPersistentProperty property, Criteria c Iterator parameters) { Range range = accessor.getDistanceRange(); - Optional distance = range.getUpperBound().getValue(); - Optional minDistance = range.getLowerBound().getValue(); + + Optional distance = range != null ? range.getUpperBound().getValue() : Optional.empty(); + Optional minDistance = range != null ? range.getLowerBound().getValue() : Optional.empty(); Point point = accessor.getGeoNearLocation(); Point pointToUse = point == null ? nextAs(parameters, Point.class) : point; 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 263b1ef8de..538e05df58 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 @@ -202,7 +202,7 @@ NearQuery nearQuery(Query query) { } Range distances = accessor.getDistanceRange(); - Assert.notNull(nearLocation, "[query.distance] must not be null"); + Assert.notNull(distances, "[query.distances] must not be null"); distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric())); distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric()));