Browse Source

Support generating geospatial queries during AOT.

Closes #5004
Original pull request: #5005
pull/5026/head
Christoph Strobl 7 months ago committed by Mark Paluch
parent
commit
b5fc92220e
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
  2. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
  3. 26
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  4. 51
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java
  5. 24
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
  6. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java
  7. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java
  8. 360
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java
  9. 180
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java
  10. 100
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/DeleteBlocks.java
  11. 145
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/GeoBlocks.java
  12. 757
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java
  13. 73
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java
  14. 68
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/NearQueryInteraction.java
  15. 303
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java
  16. 145
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateBlocks.java
  17. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java
  18. 65
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
  19. 23
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java
  20. 59
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java
  21. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java
  22. 26
      spring-data-mongodb/src/test/java/example/aot/Location.java
  23. 10
      spring-data-mongodb/src/test/java/example/aot/User.java
  24. 38
      spring-data-mongodb/src/test/java/example/aot/UserRepository.java
  25. 24
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
  26. 71
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
  27. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
  28. 9
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java
  29. 154
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java
  30. 7
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java
  31. 222
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java
  32. 4
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java
  33. 3
      spring-data-mongodb/src/test/resources/logback.xml
  34. 32
      src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc

8
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java

@ -225,6 +225,14 @@ public interface ExecutableFindOperation { @@ -225,6 +225,14 @@ public interface ExecutableFindOperation {
* @return never {@literal null}.
*/
GeoResults<T> all();
/**
* Count matching elements.
*
* @return number of elements matching the query.
* @since 5.0
*/
long count();
}
/**

5
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java

@ -243,6 +243,11 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation { @@ -243,6 +243,11 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation {
public GeoResults<G> all() {
return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter);
}
@Override
public long count() {
return template.doGeoNearCount(nearQuery, domainType, getCollectionName());
}
}
}

26
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -48,6 +48,7 @@ import org.springframework.dao.OptimisticLockingFailureException; @@ -48,6 +48,7 @@ import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Window;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
@ -1044,6 +1045,31 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -1044,6 +1045,31 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return doGeoNear(near, domainType, collectionName, returnType, QueryResultConverter.entity());
}
long doGeoNearCount(NearQuery near, Class<?> domainType, String collectionName) {
Builder optionsBuilder = AggregationOptions.builder().collation(near.getCollation());
if (near.hasReadPreference()) {
optionsBuilder.readPreference(near.getReadPreference());
}
if (near.hasReadConcern()) {
optionsBuilder.readConcern(near.getReadConcern());
}
String distanceField = operations.nearQueryDistanceFieldName(domainType);
Aggregation $geoNear = TypedAggregation.newAggregation(domainType,
Aggregation.geoNear(near, distanceField).skip(-1).limit(-1), Aggregation.count().as("_totalCount"))
.withOptions(optionsBuilder.build());
AggregationResults<Document> results = doAggregate($geoNear, collectionName, Document.class,
queryOperations.createAggregation($geoNear, (AggregationOperationContext) null));
Iterator<Document> iterator = results.iterator();
return iterator.hasNext()
? NumberUtils.convertNumberToTargetClass(iterator.next().get("_totalCount", Integer.class), Long.class)
: 0L;
}
<T, R> GeoResults<R> doGeoNear(NearQuery near, Class<?> domainType, String collectionName, Class<T> returnType,
QueryResultConverter<? super T, ? extends R> resultConverter) {

51
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java

@ -42,6 +42,8 @@ public class GeoNearOperation implements AggregationOperation { @@ -42,6 +42,8 @@ public class GeoNearOperation implements AggregationOperation {
private final NearQuery nearQuery;
private final String distanceField;
private final @Nullable String indexKey;
private final @Nullable Long skip;
private final @Nullable Integer limit;
/**
* Creates a new {@link GeoNearOperation} from the given {@link NearQuery} and the given distance field. The
@ -51,7 +53,7 @@ public class GeoNearOperation implements AggregationOperation { @@ -51,7 +53,7 @@ public class GeoNearOperation implements AggregationOperation {
* @param distanceField must not be {@literal null}.
*/
public GeoNearOperation(NearQuery nearQuery, String distanceField) {
this(nearQuery, distanceField, null);
this(nearQuery, distanceField, null, nearQuery.getSkip(), null);
}
/**
@ -63,7 +65,8 @@ public class GeoNearOperation implements AggregationOperation { @@ -63,7 +65,8 @@ public class GeoNearOperation implements AggregationOperation {
* @param indexKey can be {@literal null};
* @since 2.1
*/
private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable String indexKey) {
private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable String indexKey, @Nullable Long skip,
@Nullable Integer limit) {
Assert.notNull(nearQuery, "NearQuery must not be null");
Assert.hasLength(distanceField, "Distance field must not be null or empty");
@ -71,6 +74,8 @@ public class GeoNearOperation implements AggregationOperation { @@ -71,6 +74,8 @@ public class GeoNearOperation implements AggregationOperation {
this.nearQuery = nearQuery;
this.distanceField = distanceField;
this.indexKey = indexKey;
this.skip = skip;
this.limit = limit;
}
/**
@ -83,7 +88,30 @@ public class GeoNearOperation implements AggregationOperation { @@ -83,7 +88,30 @@ public class GeoNearOperation implements AggregationOperation {
*/
@Contract("_ -> new")
public GeoNearOperation useIndex(String key) {
return new GeoNearOperation(nearQuery, distanceField, key);
return new GeoNearOperation(nearQuery, distanceField, key, skip, limit);
}
/**
* Override potential skip applied via {@link NearQuery#getSkip()}. Adds an additional {@link SkipOperation} if value
* is non negative.
*
* @param skip
* @return new instance of {@link GeoNearOperation}.
* @since 5.0
*/
public GeoNearOperation skip(long skip) {
return new GeoNearOperation(nearQuery, distanceField, indexKey, skip, limit);
}
/**
* Override potential limit value. Adds an additional {@link LimitOperation} if value is non negative.
*
* @param limit
* @return new instance of {@link GeoNearOperation}.
* @since 5.0
*/
public GeoNearOperation limit(Integer limit) {
return new GeoNearOperation(nearQuery, distanceField, indexKey, skip, limit);
}
@Override
@ -92,7 +120,13 @@ public class GeoNearOperation implements AggregationOperation { @@ -92,7 +120,13 @@ public class GeoNearOperation implements AggregationOperation {
Document command = context.getMappedObject(nearQuery.toDocument());
if (command.containsKey("query")) {
command.replace("query", context.getMappedObject(command.get("query", Document.class)));
Document query = command.get("query", Document.class);
if (query == null || query.isEmpty()) {
command.remove("query");
} else {
command.replace("query", context.getMappedObject(query));
}
}
command.remove("collation");
@ -115,15 +149,18 @@ public class GeoNearOperation implements AggregationOperation { @@ -115,15 +149,18 @@ public class GeoNearOperation implements AggregationOperation {
Document command = toDocument(context);
Number limit = (Number) command.get("$geoNear", Document.class).remove("num");
if (limit != null && this.limit != null) {
limit = this.limit;
}
List<Document> stages = new ArrayList<>(3);
stages.add(command);
if (nearQuery.getSkip() != null && nearQuery.getSkip() > 0) {
stages.add(new Document("$skip", nearQuery.getSkip()));
if (this.skip != null && this.skip > 0) {
stages.add(new Document("$skip", this.skip));
}
if (limit != null) {
if (limit != null && limit.longValue() > 0) {
stages.add(new Document("$limit", limit.longValue()));
}

24
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java

@ -46,9 +46,7 @@ public interface CriteriaDefinition { @@ -46,9 +46,7 @@ public interface CriteriaDefinition {
* @since 5.0
* @author Christoph Strobl
*/
class Placeholder {
private final Object expression;
interface Placeholder {
/**
* Create a new placeholder for index bindable parameter.
@ -56,23 +54,29 @@ public interface CriteriaDefinition { @@ -56,23 +54,29 @@ public interface CriteriaDefinition {
* @param position the index of the parameter to bind.
* @return new instance of {@link Placeholder}.
*/
public static Placeholder indexed(int position) {
return new Placeholder("?%s".formatted(position));
static Placeholder indexed(int position) {
return new PlaceholderImpl("?%s".formatted(position));
}
public static Placeholder placeholder(String expression) {
return new Placeholder(expression);
static Placeholder placeholder(String expression) {
return new PlaceholderImpl(expression);
}
Placeholder(Object value) {
this.expression = value;
Object getValue();
}
static class PlaceholderImpl implements Placeholder {
private final Object expression;
public PlaceholderImpl(Object expression) {
this.expression = expression;
}
@Override
public Object getValue() {
return expression;
}
@Override
public String toString() {
return getValue().toString();
}

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java

@ -22,6 +22,7 @@ import org.springframework.data.geo.Box; @@ -22,6 +22,7 @@ import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.util.Assert;
@ -75,6 +76,9 @@ public final class GeoCommand { @@ -75,6 +76,9 @@ public final class GeoCommand {
Assert.notNull(shape, "Shape must not be null");
if(shape instanceof GeoJson<?>) {
return "$geometry";
}
if (shape instanceof Box) {
return "$box";
} else if (shape instanceof Circle) {

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java

@ -671,7 +671,7 @@ public final class NearQuery implements ReadConcernAware, ReadPreferenceAware { @@ -671,7 +671,7 @@ public final class NearQuery implements ReadConcernAware, ReadPreferenceAware {
document.put("distanceMultiplier", getDistanceMultiplier());
}
if (limit != null) {
if (limit != null && limit > 0) {
document.put("num", limit);
}

360
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java

@ -0,0 +1,360 @@ @@ -0,0 +1,360 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bson.Document;
import org.jspecify.annotations.NullUnmarked;
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.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 5.0
*/
class AggregationBlocks {
@NullUnmarked
static class AggregationExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String aggregationVariableName;
AggregationExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
AggregationExecutionCodeBlockBuilder referencing(String aggregationVariableName) {
this.aggregationVariableName = aggregationVariableName;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
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();
}
if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
return builder.build();
}
if (ClassUtils.isAssignable(AggregationResults.class, context.getMethod().getReturnType())) {
builder.addStatement("return $L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
return builder.build();
}
if (outputType == Document.class) {
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
if (queryMethod.isStreamQuery()) {
builder.addStatement("$T<$T> $L = $L.aggregateStream($L, $T.class)", Stream.class, Document.class,
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
builder.addStatement("return $1L.map(it -> ($2T) convertSimpleRawResult($2T.class, it))",
context.localVariable("results"), returnType);
} else {
builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
if (!queryMethod.isCollectionQuery()) {
builder.addStatement(
"return $1T.<$2T>firstElement(convertSimpleRawResults($2T.class, $3L.getMappedResults()))",
CollectionUtils.class, returnType, context.localVariable("results"));
} else {
builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType,
context.localVariable("results"));
}
}
} else {
if (queryMethod.isSliceQuery()) {
builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()",
context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName());
builder.addStatement(
"return new $1T<>($2L ? $3L.getMappedResults().subList(0, $4L.getPageSize()) : $3L.getMappedResults(), $4L, $2L)",
SliceImpl.class, context.localVariable("hasNext"), context.localVariable("results"),
context.getPageableParameterName());
} else {
if (queryMethod.isStreamQuery()) {
builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName,
outputType);
} else {
builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef,
aggregationVariableName, outputType);
}
}
}
return builder.build();
}
}
@NullUnmarked
static class AggregationCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private final List<CodeBlock> arguments;
private AggregationInteraction source;
private String aggregationVariableName;
private boolean pipelineOnly;
AggregationCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.arguments = context.getBindableParameterNames().stream().map(CodeBlock::of).collect(Collectors.toList());
this.queryMethod = queryMethod;
}
AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) {
this.source = aggregation;
return this;
}
AggregationCodeBlockBuilder usingAggregationVariableName(String aggregationVariableName) {
this.aggregationVariableName = aggregationVariableName;
return this;
}
AggregationCodeBlockBuilder pipelineOnly(boolean pipelineOnly) {
this.pipelineOnly = pipelineOnly;
return this;
}
CodeBlock build() {
Builder builder = CodeBlock.builder();
builder.add("\n");
String pipelineName = context.localVariable(aggregationVariableName + (pipelineOnly ? "" : "Pipeline"));
builder.add(pipeline(pipelineName));
if (!pipelineOnly) {
builder.addStatement("$1T<$2T> $3L = $4T.newAggregation($2T.class, $5L.getOperations())",
TypedAggregation.class, context.getRepositoryInformation().getDomainType(), aggregationVariableName,
Aggregation.class, pipelineName);
builder.add(aggregationOptions(aggregationVariableName));
}
return builder.build();
}
private CodeBlock pipeline(String pipelineVariableName) {
String sortParameter = context.getSortParameterName();
String limitParameter = context.getLimitParameterName();
String pageableParameter = context.getPageableParameterName();
boolean mightBeSorted = StringUtils.hasText(sortParameter);
boolean mightBeLimited = StringUtils.hasText(limitParameter);
boolean mightBePaged = StringUtils.hasText(pageableParameter);
int stageCount = source.stages().size();
if (mightBeSorted) {
stageCount++;
}
if (mightBeLimited) {
stageCount++;
}
if (mightBePaged) {
stageCount += 3;
}
Builder builder = CodeBlock.builder();
builder.add(aggregationStages(context.localVariable("stages"), source.stages(), stageCount, arguments));
if (mightBeSorted) {
builder.add(sortingStage(sortParameter));
}
if (mightBeLimited) {
builder.add(limitingStage(limitParameter));
}
if (mightBePaged) {
builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery()));
}
builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName,
context.localVariable("stages"));
return builder.build();
}
private CodeBlock aggregationOptions(String aggregationVariableName) {
Builder builder = CodeBlock.builder();
List<CodeBlock> options = new ArrayList<>(5);
if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
options.add(CodeBlock.of(".skipOutput()"));
}
MergedAnnotation<Hint> hintAnnotation = context.getAnnotation(Hint.class);
String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null;
if (StringUtils.hasText(hint)) {
options.add(CodeBlock.of(".hint($S)", hint));
}
MergedAnnotation<ReadPreference> readPreferenceAnnotation = context.getAnnotation(ReadPreference.class);
String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null;
if (StringUtils.hasText(readPreference)) {
options.add(CodeBlock.of(".readPreference($T.valueOf($S))", com.mongodb.ReadPreference.class, readPreference));
}
if (queryMethod.hasAnnotatedCollation()) {
options.add(CodeBlock.of(".collation($T.parse($S))", Collation.class, queryMethod.getAnnotatedCollation()));
}
if (!options.isEmpty()) {
Builder optionsBuilder = CodeBlock.builder();
optionsBuilder.add("$1T $2L = $1T.builder()\n", AggregationOptions.class,
context.localVariable("aggregationOptions"));
optionsBuilder.indent();
for (CodeBlock optionBlock : options) {
optionsBuilder.add(optionBlock);
optionsBuilder.add("\n");
}
optionsBuilder.add(".build();\n");
optionsBuilder.unindent();
builder.add(optionsBuilder.build());
builder.addStatement("$1L = $1L.withOptions($2L)", aggregationVariableName,
context.localVariable("aggregationOptions"));
}
return builder.build();
}
private CodeBlock aggregationStages(String stageListVariableName, Iterable<String> stages, int stageCount,
List<CodeBlock> arguments) {
Builder builder = CodeBlock.builder();
builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class,
stageCount);
int stageCounter = 0;
for (String stage : stages) {
String stageName = context.localVariable("stage_%s".formatted(stageCounter++));
builder.add(MongoCodeBlocks.renderExpressionToDocument(stage, stageName, arguments));
builder.addStatement("$L.add($L)", context.localVariable("stages"), stageName);
}
return builder.build();
}
private CodeBlock sortingStage(String sortProvider) {
Builder builder = CodeBlock.builder();
builder.beginControlFlow("if ($L.isSorted())", sortProvider);
builder.addStatement("$1T $2L = new $1T()", Document.class, context.localVariable("sortDocument"));
builder.beginControlFlow("for ($T $L : $L)", Order.class, context.localVariable("order"), 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"));
builder.endControlFlow();
return builder.build();
}
private CodeBlock pagingStage(String pageableProvider, boolean slice) {
Builder builder = CodeBlock.builder();
builder.add(sortingStage(pageableProvider + ".getSort()"));
builder.beginControlFlow("if ($L.isPaged())", pageableProvider);
builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider);
builder.addStatement("$L.add($T.skip($L.getOffset()))", context.localVariable("stages"), Aggregation.class,
pageableProvider);
builder.endControlFlow();
if (slice) {
builder.addStatement("$L.add($T.limit($L.getPageSize() + 1))", context.localVariable("stages"),
Aggregation.class, pageableProvider);
} else {
builder.addStatement("$L.add($T.limit($L.getPageSize()))", context.localVariable("stages"), Aggregation.class,
pageableProvider);
}
builder.endControlFlow();
return builder.build();
}
private CodeBlock limitingStage(String limitProvider) {
Builder builder = CodeBlock.builder();
builder.beginControlFlow("if ($L.isLimited())", limitProvider);
builder.addStatement("$L.add($T.limit($L.max()))", context.localVariable("stages"), Aggregation.class,
limitProvider);
builder.endControlFlow();
return builder.build();
}
}
}

180
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java

@ -15,10 +15,9 @@ @@ -15,10 +15,9 @@
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.bson.conversions.Bson;
import org.jspecify.annotations.NullUnmarked;
@ -29,10 +28,17 @@ import org.springframework.data.domain.Score; @@ -29,10 +28,17 @@ import org.springframework.data.domain.Score;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Vector;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Collation;
@ -43,8 +49,13 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -43,8 +49,13 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoQueryCreator;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.ClassUtils;
import com.mongodb.DBRef;
@ -68,10 +79,13 @@ class AotQueryCreator { @@ -68,10 +79,13 @@ class AotQueryCreator {
}
@SuppressWarnings("NullAway")
StringQuery createQuery(PartTree partTree, int parameterCount) {
StringQuery createQuery(PartTree partTree, QueryMethod queryMethod) {
boolean geoNear = queryMethod instanceof MongoQueryMethod mqm ? mqm.isGeoNearQuery() : false;
Query query = new MongoQueryCreator(partTree,
new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext)
new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(queryMethod)), mappingContext, geoNear, queryMethod.isSearchQuery())
.createQuery();
if (partTree.isLimiting()) {
@ -118,17 +132,37 @@ class AotQueryCreator { @@ -118,17 +132,37 @@ class AotQueryCreator {
private final List<Placeholder> placeholders;
public PlaceholderParameterAccessor(int parameterCount) {
if (parameterCount == 0) {
public PlaceholderParameterAccessor(QueryMethod queryMethod) {
if (queryMethod.getParameters().getNumberOfParameters() == 0) {
placeholders = List.of();
} else {
placeholders = IntStream.range(0, parameterCount).mapToObj(Placeholder::indexed).collect(Collectors.toList());
placeholders = new ArrayList<>();
Parameters<?, ?> parameters = queryMethod.getParameters();
for (Parameter parameter : parameters.toList()) {
if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), new GeoJsonPlaceholder(parameter.getIndex(), ""));
}
else if (ClassUtils.isAssignable(Point.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), new PointPlaceholder(parameter.getIndex()));
} else if (ClassUtils.isAssignable(Circle.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), new CirclePlaceholder(parameter.getIndex()));
} else if (ClassUtils.isAssignable(Box.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), new BoxPlaceholder(parameter.getIndex()));
} else if (ClassUtils.isAssignable(Sphere.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), new SpherePlaceholder(parameter.getIndex()));
} else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), new PolygonPlaceholder(parameter.getIndex()));
}
else {
placeholders.add(parameter.getIndex(), Placeholder.indexed(parameter.getIndex()));
}
}
}
}
@Override
public Range<Distance> getDistanceRange() {
return null;
return Range.unbounded();
}
@Override
@ -207,4 +241,134 @@ class AotQueryCreator { @@ -207,4 +241,134 @@ class AotQueryCreator {
return ((List) placeholders).iterator();
}
}
static class CirclePlaceholder extends Circle implements Placeholder {
int index;
public CirclePlaceholder(int index) {
super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); //
this.index = index;
}
@Override
public Object getValue() {
return "?%s".formatted(index);
}
@Override
public String toString() {
return getValue().toString();
}
}
static class SpherePlaceholder extends Sphere implements Placeholder {
int index;
public SpherePlaceholder(int index) {
super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); //
this.index = index;
}
@Override
public Object getValue() {
return "?%s".formatted(index);
}
@Override
public String toString() {
return getValue().toString();
}
}
static class GeoJsonPlaceholder implements Placeholder, GeoJson<List<Placeholder>>, Shape {
int index;
String type;
public GeoJsonPlaceholder(int index, String type) {
this.index = index;
this.type = type;
}
@Override
public Object getValue() {
return "?%s".formatted(index);
}
@Override
public String toString() {
return getValue().toString();
}
@Override
public String getType() {
return type;
}
@Override
public List<Placeholder> getCoordinates() {
return List.of();
}
}
static class BoxPlaceholder extends Box implements Placeholder {
int index;
public BoxPlaceholder(int index) {
super(new PointPlaceholder(index), new PointPlaceholder(index));
this.index = index;
}
@Override
public Object getValue() {
return "?%s".formatted(index);
}
@Override
public String toString() {
return getValue().toString();
}
}
static class PolygonPlaceholder extends Polygon implements Placeholder {
int index;
public PolygonPlaceholder(int index) {
super(new PointPlaceholder(index), new PointPlaceholder(index), new PointPlaceholder(index),
new PointPlaceholder(index));
this.index = index;
}
@Override
public Object getValue() {
return "?%s".formatted(index);
}
@Override
public String toString() {
return getValue().toString();
}
}
static class PointPlaceholder extends Point implements Placeholder {
int index;
public PointPlaceholder(int index) {
super(Double.NaN, Double.NaN);
this.index = index;
}
@Override
public Object getValue() {
return "?" + index;
}
@Override
public String toString() {
return getValue().toString();
}
}
}

100
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/DeleteBlocks.java

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.Optional;
import org.jspecify.annotations.NullUnmarked;
import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
* @since 5.0
*/
class DeleteBlocks {
@NullUnmarked
static class DeleteExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String queryVariableName;
DeleteExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
DeleteExecutionCodeBlockBuilder referencing(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
Class<?> domainType = context.getRepositoryInformation().getDomainType();
boolean isProjecting = context.getActualReturnType() != null
&& !ObjectUtils.nullSafeEquals(TypeName.get(domainType), context.getActualReturnType());
Object actualReturnType = isProjecting ? context.getActualReturnType().getType() : domainType;
builder.add("\n");
builder.addStatement("$1T<$2T> $3L = $4L.remove($2T.class)", ExecutableRemove.class, domainType,
context.localVariable("remover"), mongoOpsRef);
DeleteExecution.Type type = DeleteExecution.Type.FIND_AND_REMOVE_ALL;
if (!queryMethod.isCollectionQuery()) {
if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) {
type = DeleteExecution.Type.FIND_AND_REMOVE_ONE;
} else {
type = DeleteExecution.Type.ALL;
}
}
actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())
? TypeName.get(context.getMethod().getReturnType())
: queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType;
if (ClassUtils.isVoidType(context.getMethod().getReturnType())) {
builder.addStatement("new $T($L, $T.$L).execute($L)", DeleteExecution.class, context.localVariable("remover"),
DeleteExecution.Type.class, type.name(), queryVariableName);
} else if (context.getMethod().getReturnType() == Optional.class) {
builder.addStatement("return $T.ofNullable(($T) new $T($L, $T.$L).execute($L))", Optional.class,
actualReturnType, DeleteExecution.class, context.localVariable("remover"), DeleteExecution.Type.class,
type.name(), queryVariableName);
} else {
builder.addStatement("return ($T) new $T($L, $T.$L).execute($L)", actualReturnType, DeleteExecution.class,
context.localVariable("remover"), DeleteExecution.Type.class, type.name(), queryVariableName);
}
return builder.build();
}
}
}

145
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/GeoBlocks.java

@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.aot;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoPage;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.util.ClassUtils;
/**
* @author Christoph Strobl
* @since 5.0
*/
class GeoBlocks {
static class GeoNearCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String variableName;
GeoNearCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
String locationParameterName = context.getParameterName(queryMethod.getParameters().getNearIndex());
builder.addStatement("$1T $2L = $1T.near($3L)", NearQuery.class, variableName, locationParameterName);
if (queryMethod.getParameters().getRangeIndex() != -1) {
String rangeParametername = context.getParameterName(queryMethod.getParameters().getRangeIndex());
String minVarName = context.localVariable("min");
String maxVarName = context.localVariable("max");
builder.beginControlFlow("if($L.getLowerBound().isBounded())", rangeParametername);
builder.addStatement("$1T $2L = $3L.getLowerBound().getValue().get()", Distance.class, minVarName,
rangeParametername);
builder.addStatement("$1L.minDistance($2L).in($2L.getMetric())", variableName, minVarName);
builder.endControlFlow();
builder.beginControlFlow("if($L.getUpperBound().isBounded())", rangeParametername);
builder.addStatement("$1T $2L = $3L.getUpperBound().getValue().get()", Distance.class, maxVarName,
rangeParametername);
builder.addStatement("$1L.maxDistance($2L).in($2L.getMetric())", variableName, maxVarName);
builder.endControlFlow();
} else {
String distanceParametername = context.getParameterName(queryMethod.getParameters().getMaxDistanceIndex());
builder.addStatement("$1L.maxDistance($2L).in($2L.getMetric())", variableName, distanceParametername);
}
if (context.getPageableParameterName() != null) {
builder.addStatement("$L.with($L)", variableName, context.getPageableParameterName());
}
MongoCodeBlocks.appendReadPreference(context, builder, variableName);
return builder.build();
}
public GeoNearCodeBlockBuilder usingQueryVariableName(String variableName) {
this.variableName = variableName;
return this;
}
}
static class GeoNearExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String queryVariableName;
GeoNearExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
GeoNearExecutionCodeBlockBuilder referencing(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
String executorVar = context.localVariable("nearFinder");
builder.addStatement("var $L = $L.query($T.class).near($L)", executorVar,
context.fieldNameOf(MongoOperations.class), context.getRepositoryInformation().getDomainType(),
queryVariableName);
if (ClassUtils.isAssignable(GeoPage.class, context.getReturnType().getRawClass())) {
String geoResultVar = context.localVariable("geoResult");
builder.addStatement("var $L = $L.all()", geoResultVar, executorVar);
builder.beginControlFlow("if($L.isUnpaged())", context.getPageableParameterName());
builder.addStatement("return new $T<>($L)", GeoPage.class, geoResultVar);
builder.endControlFlow();
String pageVar = context.localVariable("resultPage");
builder.addStatement("var $L = $T.getPage($L.getContent(), $L, () -> $L.count())", pageVar,
PageableExecutionUtils.class, geoResultVar, context.getPageableParameterName(), executorVar);
builder.addStatement("return new $T<>($L, $L, $L.getTotalElements())", GeoPage.class, geoResultVar,
context.getPageableParameterName(), pageVar);
} else if (ClassUtils.isAssignable(GeoResults.class, context.getReturnType().getRawClass())) {
builder.addStatement("return $L.all()", executorVar);
} else {
builder.addStatement("return $L.all().getContent()", executorVar);
}
return builder.build();
}
}
}

757
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java

@ -15,48 +15,27 @@ @@ -15,48 +15,27 @@
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.bson.Document;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
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.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.BasicUpdate;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution;
import org.springframework.data.mongodb.repository.aot.AggregationBlocks.AggregationCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.AggregationBlocks.AggregationExecutionCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.DeleteBlocks.DeleteExecutionCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.GeoBlocks.GeoNearCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.GeoBlocks.GeoNearExecutionCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.QueryBlocks.QueryCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.QueryBlocks.QueryExecutionCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.UpdateBlocks.UpdateCodeBlockBuilder;
import org.springframework.data.mongodb.repository.aot.UpdateBlocks.UpdateExecutionCodeBlockBuilder;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@ -78,6 +57,7 @@ class MongoCodeBlocks { @@ -78,6 +57,7 @@ class MongoCodeBlocks {
*/
static QueryCodeBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new QueryCodeBlockBuilder(context, queryMethod);
}
@ -116,6 +96,7 @@ class MongoCodeBlocks { @@ -116,6 +96,7 @@ class MongoCodeBlocks {
*/
static UpdateCodeBlockBuilder updateBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new UpdateCodeBlockBuilder(context, queryMethod);
}
@ -158,693 +139,67 @@ class MongoCodeBlocks { @@ -158,693 +139,67 @@ class MongoCodeBlocks {
return new AggregationExecutionCodeBlockBuilder(context, queryMethod);
}
@NullUnmarked
static class DeleteExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String queryVariableName;
DeleteExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
DeleteExecutionCodeBlockBuilder referencing(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
Class<?> domainType = context.getRepositoryInformation().getDomainType();
boolean isProjecting = context.getActualReturnType() != null
&& !ObjectUtils.nullSafeEquals(TypeName.get(domainType), context.getActualReturnType());
Object actualReturnType = isProjecting ? context.getActualReturnType().getType() : domainType;
builder.add("\n");
builder.addStatement("$T<$T> $L = $L.remove($T.class)", ExecutableRemove.class, domainType,
context.localVariable("remover"), mongoOpsRef, domainType);
DeleteExecution.Type type = DeleteExecution.Type.FIND_AND_REMOVE_ALL;
if (!queryMethod.isCollectionQuery()) {
if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) {
type = DeleteExecution.Type.FIND_AND_REMOVE_ONE;
} else {
type = DeleteExecution.Type.ALL;
}
}
actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())
? TypeName.get(context.getMethod().getReturnType())
: queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType;
if (ClassUtils.isVoidType(context.getMethod().getReturnType())) {
builder.addStatement("new $T($L, $T.$L).execute($L)", DeleteExecution.class, context.localVariable("remover"),
DeleteExecution.Type.class, type.name(), queryVariableName);
} else if (context.getMethod().getReturnType() == Optional.class) {
builder.addStatement("return $T.ofNullable(($T) new $T($L, $T.$L).execute($L))", Optional.class,
actualReturnType, DeleteExecution.class, context.localVariable("remover"), DeleteExecution.Type.class,
type.name(), queryVariableName);
} else {
builder.addStatement("return ($T) new $T($L, $T.$L).execute($L)", actualReturnType, DeleteExecution.class,
context.localVariable("remover"), DeleteExecution.Type.class, type.name(), queryVariableName);
}
return builder.build();
}
}
@NullUnmarked
static class UpdateExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String queryVariableName;
private String updateVariableName;
UpdateExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
UpdateExecutionCodeBlockBuilder withFilter(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
UpdateExecutionCodeBlockBuilder referencingUpdate(String updateVariableName) {
this.updateVariableName = updateVariableName;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
builder.add("\n");
String updateReference = updateVariableName;
Class<?> domainType = context.getRepositoryInformation().getDomainType();
builder.addStatement("$T<$T> $L = $L.update($T.class)", ExecutableUpdate.class, domainType,
context.localVariable("updater"), mongoOpsRef, domainType);
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
if (ReflectionUtils.isVoid(returnType)) {
builder.addStatement("$L.matching($L).apply($L).all()", context.localVariable("updater"), queryVariableName,
updateReference);
} else if (ClassUtils.isAssignable(Long.class, returnType)) {
builder.addStatement("return $L.matching($L).apply($L).all().getModifiedCount()",
context.localVariable("updater"), queryVariableName, updateReference);
} else {
builder.addStatement("$T $L = $L.matching($L).apply($L).all().getModifiedCount()", Long.class,
context.localVariable("modifiedCount"), context.localVariable("updater"), queryVariableName,
updateReference);
builder.addStatement("return $T.convertNumberToTargetClass($L, $T.class)", NumberUtils.class,
context.localVariable("modifiedCount"), returnType);
}
return builder.build();
}
}
@NullUnmarked
static class AggregationExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String aggregationVariableName;
AggregationExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
AggregationExecutionCodeBlockBuilder referencing(String aggregationVariableName) {
this.aggregationVariableName = aggregationVariableName;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
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();
}
if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
return builder.build();
}
if (ClassUtils.isAssignable(AggregationResults.class, context.getMethod().getReturnType())) {
builder.addStatement("return $L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
return builder.build();
}
if (outputType == Document.class) {
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
if (queryMethod.isStreamQuery()) {
builder.addStatement("$T<$T> $L = $L.aggregateStream($L, $T.class)", Stream.class, Document.class,
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
builder.addStatement("return $L.map(it -> ($T) convertSimpleRawResult($T.class, it))",
context.localVariable("results"), returnType, returnType);
} else {
builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
if (!queryMethod.isCollectionQuery()) {
builder.addStatement("return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))",
CollectionUtils.class, returnType, returnType, context.localVariable("results"));
} else {
builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType,
context.localVariable("results"));
}
}
} else {
if (queryMethod.isSliceQuery()) {
builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class,
context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType);
builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()",
context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName());
builder.addStatement(
"return new $T<>($L ? $L.getMappedResults().subList(0, $L.getPageSize()) : $L.getMappedResults(), $L, $L)",
SliceImpl.class, context.localVariable("hasNext"), context.localVariable("results"),
context.getPageableParameterName(), context.localVariable("results"), context.getPageableParameterName(),
context.localVariable("hasNext"));
} else {
if (queryMethod.isStreamQuery()) {
builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName,
outputType);
} else {
builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef,
aggregationVariableName, outputType);
}
}
}
/**
* Builder for generating {@link org.springframework.data.mongodb.core.query.NearQuery} {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return
*/
static GeoNearCodeBlockBuilder geoNearBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return builder.build();
}
return new GeoNearCodeBlockBuilder(context, queryMethod);
}
@NullUnmarked
static class QueryExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private QueryInteraction query;
QueryExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
QueryExecutionCodeBlockBuilder forQuery(QueryInteraction query) {
this.query = query;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
boolean isProjecting = context.getReturnedType().isProjecting();
Class<?> domainType = context.getRepositoryInformation().getDomainType();
Object actualReturnType = queryMethod.getParameters().hasDynamicProjection() || isProjecting
? TypeName.get(context.getActualReturnType().getType())
: domainType;
builder.add("\n");
if (queryMethod.getParameters().hasDynamicProjection()) {
builder.addStatement("$T<$T> $L = $L.query($T.class).as($L)", FindWithQuery.class, actualReturnType,
context.localVariable("finder"), mongoOpsRef, domainType, context.getDynamicProjectionParameterName());
} else if (isProjecting) {
builder.addStatement("$T<$T> $L = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType,
context.localVariable("finder"), mongoOpsRef, domainType, actualReturnType);
} else {
builder.addStatement("$T<$T> $L = $L.query($T.class)", FindWithQuery.class, actualReturnType,
context.localVariable("finder"), mongoOpsRef, domainType);
}
String terminatingMethod;
if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
terminatingMethod = "all()";
} else if (query.isCount()) {
terminatingMethod = "count()";
} else if (query.isExists()) {
terminatingMethod = "exists()";
} else if (queryMethod.isStreamQuery()) {
terminatingMethod = "stream()";
} else {
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()";
}
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()) {
builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class,
context.localVariable("finder"), context.getPageableParameterName(), query.name());
} else if (queryMethod.isScrollQuery()) {
String scrollPositionParameterName = context.getScrollPositionParameterName();
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(),
scrollPositionParameterName);
} else {
if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) {
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
builder.addStatement("return $T.convertNumberToTargetClass($L.matching($L).$L, $T.class)", NumberUtils.class,
context.localVariable("finder"), query.name(), terminatingMethod, returnType);
} else {
builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(),
terminatingMethod);
}
}
/**
* Builder for generating {@link org.springframework.data.mongodb.core.query.NearQuery} execution {@link CodeBlock}
* that can return {@link org.springframework.data.geo.GeoResults}.
*
* @param context
* @param queryMethod
* @return
*/
static GeoNearExecutionCodeBlockBuilder geoNearExecutionBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return builder.build();
}
return new GeoNearExecutionCodeBlockBuilder(context, queryMethod);
}
@NullUnmarked
static class AggregationCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private AggregationInteraction source;
private final List<String> arguments;
private String aggregationVariableName;
private boolean pipelineOnly;
AggregationCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.arguments = context.getBindableParameterNames();
this.queryMethod = queryMethod;
}
AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) {
this.source = aggregation;
return this;
}
AggregationCodeBlockBuilder usingAggregationVariableName(String aggregationVariableName) {
this.aggregationVariableName = aggregationVariableName;
return this;
}
AggregationCodeBlockBuilder pipelineOnly(boolean pipelineOnly) {
this.pipelineOnly = pipelineOnly;
return this;
}
CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
String pipelineName = context.localVariable(aggregationVariableName + (pipelineOnly ? "" : "Pipeline"));
builder.add(pipeline(pipelineName));
static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, List<CodeBlock> arguments) {
if (!pipelineOnly) {
builder.addStatement("$T<$T> $L = $T.newAggregation($T.class, $L.getOperations())", TypedAggregation.class,
context.getRepositoryInformation().getDomainType(), aggregationVariableName, Aggregation.class,
context.getRepositoryInformation().getDomainType(), pipelineName);
builder.add(aggregationOptions(aggregationVariableName));
}
return builder.build();
}
private CodeBlock pipeline(String pipelineVariableName) {
String sortParameter = context.getSortParameterName();
String limitParameter = context.getLimitParameterName();
String pageableParameter = context.getPageableParameterName();
boolean mightBeSorted = StringUtils.hasText(sortParameter);
boolean mightBeLimited = StringUtils.hasText(limitParameter);
boolean mightBePaged = StringUtils.hasText(pageableParameter);
int stageCount = source.stages().size();
if (mightBeSorted) {
stageCount++;
}
if (mightBeLimited) {
stageCount++;
}
if (mightBePaged) {
stageCount += 3;
}
Builder builder = CodeBlock.builder();
builder.add(aggregationStages(context.localVariable("stages"), source.stages(), stageCount, arguments));
if (mightBeSorted) {
builder.add(sortingStage(sortParameter));
}
if (mightBeLimited) {
builder.add(limitingStage(limitParameter));
}
if (mightBePaged) {
builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery()));
}
builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName,
context.localVariable("stages"));
return builder.build();
}
private CodeBlock aggregationOptions(String aggregationVariableName) {
Builder builder = CodeBlock.builder();
List<CodeBlock> options = new ArrayList<>(5);
if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
options.add(CodeBlock.of(".skipOutput()"));
}
MergedAnnotation<Hint> hintAnnotation = context.getAnnotation(Hint.class);
String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null;
if (StringUtils.hasText(hint)) {
options.add(CodeBlock.of(".hint($S)", hint));
}
MergedAnnotation<ReadPreference> readPreferenceAnnotation = context.getAnnotation(ReadPreference.class);
String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null;
if (StringUtils.hasText(readPreference)) {
options.add(CodeBlock.of(".readPreference($T.valueOf($S))", com.mongodb.ReadPreference.class, readPreference));
}
if (queryMethod.hasAnnotatedCollation()) {
options.add(CodeBlock.of(".collation($T.parse($S))", Collation.class, queryMethod.getAnnotatedCollation()));
}
if (!options.isEmpty()) {
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.addStatement("$1T $2L = new $1T()", Document.class, variableName);
} else if (!containsPlaceholder(source)) {
builder.addStatement("$1T $2L = $1T.parse($3S)", Document.class, variableName, source);
} else {
Builder optionsBuilder = CodeBlock.builder();
optionsBuilder.add("$T $L = $T.builder()\n", AggregationOptions.class,
context.localVariable("aggregationOptions"), AggregationOptions.class);
optionsBuilder.indent();
for (CodeBlock optionBlock : options) {
optionsBuilder.add(optionBlock);
optionsBuilder.add("\n");
builder.add("$T $L = bindParameters($S, new $T[]{ ", Document.class, variableName, source, Object.class);
Iterator<CodeBlock> iterator = arguments.iterator();
while (iterator.hasNext()) {
builder.add(iterator.next());
if (iterator.hasNext()) {
builder.add(", ");
}
optionsBuilder.add(".build();\n");
optionsBuilder.unindent();
builder.add(optionsBuilder.build());
builder.addStatement("$L = $L.withOptions($L)", aggregationVariableName, aggregationVariableName,
context.localVariable("aggregationOptions"));
}
return builder.build();
}
private CodeBlock aggregationStages(String stageListVariableName, Iterable<String> stages, int stageCount,
List<String> arguments) {
Builder builder = CodeBlock.builder();
builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class,
stageCount);
int stageCounter = 0;
for (String stage : stages) {
String stageName = context.localVariable("stage_%s".formatted(stageCounter++));
builder.add(renderExpressionToDocument(stage, stageName, arguments));
builder.addStatement("$L.add($L)", context.localVariable("stages"), stageName);
}
return builder.build();
}
private CodeBlock sortingStage(String sortProvider) {
Builder builder = CodeBlock.builder();
builder.beginControlFlow("if ($L.isSorted())", sortProvider);
builder.addStatement("$T $L = new $T()", Document.class, context.localVariable("sortDocument"), Document.class);
builder.beginControlFlow("for ($T $L : $L)", Order.class, context.localVariable("order"), sortProvider);
builder.addStatement("$L.append($L.getProperty(), $L.isAscending() ? 1 : -1);",
context.localVariable("sortDocument"), context.localVariable("order"), context.localVariable("order"));
builder.endControlFlow();
builder.addStatement("stages.add(new $T($S, $L))", Document.class, "$sort",
context.localVariable("sortDocument"));
builder.endControlFlow();
return builder.build();
}
private CodeBlock pagingStage(String pageableProvider, boolean slice) {
Builder builder = CodeBlock.builder();
builder.add(sortingStage(pageableProvider + ".getSort()"));
builder.beginControlFlow("if ($L.isPaged())", pageableProvider);
builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider);
builder.addStatement("$L.add($T.skip($L.getOffset()))", context.localVariable("stages"), Aggregation.class,
pageableProvider);
builder.endControlFlow();
if (slice) {
builder.addStatement("$L.add($T.limit($L.getPageSize() + 1))", context.localVariable("stages"),
Aggregation.class, pageableProvider);
} else {
builder.addStatement("$L.add($T.limit($L.getPageSize()))", context.localVariable("stages"), Aggregation.class,
pageableProvider);
}
builder.endControlFlow();
return builder.build();
}
private CodeBlock limitingStage(String limitProvider) {
Builder builder = CodeBlock.builder();
builder.beginControlFlow("if ($L.isLimited())", limitProvider);
builder.addStatement("$L.add($T.limit($L.max()))", context.localVariable("stages"), Aggregation.class,
limitProvider);
builder.endControlFlow();
return builder.build();
builder.add("});\n");
}
return builder.build();
}
@NullUnmarked
static class QueryCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private QueryInteraction source;
private final List<String> arguments;
private String queryVariableName;
QueryCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.arguments = context.getBindableParameterNames();
this.queryMethod = queryMethod;
}
QueryCodeBlockBuilder filter(QueryInteraction query) {
this.source = query;
return this;
}
QueryCodeBlockBuilder usingQueryVariableName(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
builder.add(renderExpressionToQuery(source.getQuery().getQueryString(), queryVariableName));
if (StringUtils.hasText(source.getQuery().getFieldsString())) {
builder.add(renderExpressionToDocument(source.getQuery().getFieldsString(), "fields", arguments));
builder.addStatement("$L.setFieldsObject(fields)", queryVariableName);
}
String sortParameter = context.getSortParameterName();
if (StringUtils.hasText(sortParameter)) {
builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
} else if (StringUtils.hasText(source.getQuery().getSortString())) {
builder.add(renderExpressionToDocument(source.getQuery().getSortString(), "sort", arguments));
builder.addStatement("$L.setSortObject(sort)", queryVariableName);
}
String limitParameter = context.getLimitParameterName();
if (StringUtils.hasText(limitParameter)) {
builder.addStatement("$L.limit($L)", queryVariableName, limitParameter);
} else if (context.getPageableParameterName() == null && source.getQuery().isLimited()) {
builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit());
}
String pageableParameter = context.getPageableParameterName();
if (StringUtils.hasText(pageableParameter) && !queryMethod.isPageQuery() && !queryMethod.isSliceQuery()) {
builder.addStatement("$L.with($L)", queryVariableName, pageableParameter);
}
MergedAnnotation<Hint> hintAnnotation = context.getAnnotation(Hint.class);
String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null;
if (StringUtils.hasText(hint)) {
builder.addStatement("$L.withHint($S)", queryVariableName, hint);
}
MergedAnnotation<ReadPreference> readPreferenceAnnotation = context.getAnnotation(ReadPreference.class);
String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null;
if (StringUtils.hasText(readPreference)) {
builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName,
com.mongodb.ReadPreference.class, readPreference);
}
MergedAnnotation<Meta> metaAnnotation = context.getAnnotation(Meta.class);
if (metaAnnotation.isPresent()) {
long maxExecutionTimeMs = metaAnnotation.getLong("maxExecutionTimeMs");
if (maxExecutionTimeMs != -1) {
builder.addStatement("$L.maxTimeMsec($L)", queryVariableName, maxExecutionTimeMs);
}
int cursorBatchSize = metaAnnotation.getInt("cursorBatchSize");
if (cursorBatchSize != 0) {
builder.addStatement("$L.cursorBatchSize($L)", queryVariableName, cursorBatchSize);
}
String comment = metaAnnotation.getString("comment");
if (StringUtils.hasText("comment")) {
builder.addStatement("$L.comment($S)", queryVariableName, comment);
}
}
// TODO: Meta annotation: Disk usage
return builder.build();
}
private CodeBlock renderExpressionToQuery(@Nullable String source, String variableName) {
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.addStatement("$T $L = new $T(new $T())", BasicQuery.class, variableName, BasicQuery.class,
Document.class);
} else if (!containsPlaceholder(source)) {
builder.addStatement("$T $L = new $T($T.parse($S))", BasicQuery.class, variableName, BasicQuery.class,
Document.class, source);
} else {
builder.addStatement("$T $L = createQuery($S, new $T[]{ $L })", BasicQuery.class, variableName, source,
Object.class, StringUtils.collectionToDelimitedString(arguments, ", "));
}
return builder.build();
}
static boolean containsPlaceholder(String source) {
return PARAMETER_BINDING_PATTERN.matcher(source).find();
}
@NullUnmarked
static class UpdateCodeBlockBuilder {
private UpdateInteraction source;
private List<String> arguments;
private String updateVariableName;
public UpdateCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.arguments = context.getBindableParameterNames();
}
public UpdateCodeBlockBuilder update(UpdateInteraction update) {
this.source = update;
return this;
}
public UpdateCodeBlockBuilder usingUpdateVariableName(String updateVariableName) {
this.updateVariableName = updateVariableName;
return this;
}
CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
String tmpVariableName = updateVariableName + "Document";
builder.add(renderExpressionToDocument(source.getUpdate().getUpdateString(), tmpVariableName, arguments));
builder.addStatement("$T $L = new $T($L)", BasicUpdate.class, updateVariableName, BasicUpdate.class,
tmpVariableName);
return builder.build();
}
}
static void appendReadPreference(AotQueryMethodGenerationContext context, Builder builder, String queryVariableName) {
private static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName,
List<String> arguments) {
MergedAnnotation<ReadPreference> readPreferenceAnnotation = context.getAnnotation(ReadPreference.class);
String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null;
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.addStatement("$T $L = new $T()", Document.class, variableName, Document.class);
} else if (!containsPlaceholder(source)) {
builder.addStatement("$T $L = $T.parse($S)", Document.class, variableName, Document.class, source);
} else {
builder.addStatement("$T $L = bindParameters($S, new $T[]{ $L })", Document.class, variableName, source,
Object.class, StringUtils.collectionToDelimitedString(arguments, ", "));
if (StringUtils.hasText(readPreference)) {
builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName,
com.mongodb.ReadPreference.class, readPreference);
}
return builder.build();
}
private static boolean containsPlaceholder(String source) {
return PARAMETER_BINDING_PATTERN.matcher(source).find();
}
}

73
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java

@ -15,7 +15,16 @@ @@ -15,7 +15,16 @@
*/
package org.springframework.data.mongodb.repository.aot;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.*;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.aggregationBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.aggregationExecutionBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.deleteExecutionBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.geoNearBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.geoNearExecutionBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.queryBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.queryExecutionBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.updateBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.updateExecutionBlockBuilder;
import static org.springframework.data.mongodb.repository.aot.QueryBlocks.QueryCodeBlockBuilder;
import java.lang.reflect.Method;
import java.util.Locale;
@ -90,13 +99,23 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -90,13 +99,23 @@ public class MongoRepositoryContributor extends RepositoryContributor {
MongoQueryMethod queryMethod = new MongoQueryMethod(method, getRepositoryInformation(), getProjectionFactory(),
mappingContext);
if (backoff(queryMethod)) {
return null;
}
if (queryMethod.hasAnnotatedAggregation()) {
AggregationInteraction aggregation = new AggregationInteraction(queryMethod.getAnnotatedAggregation());
return aggregationMethodContributor(queryMethod, aggregation);
}
QueryInteraction query = createStringQuery(getRepositoryInformation(), queryMethod,
AnnotatedElementUtils.findMergedAnnotation(method, Query.class), method.getParameterCount());
AnnotatedElementUtils.findMergedAnnotation(method, Query.class));
if (queryMethod.isGeoNearQuery() || (queryMethod.getParameters().getMaxDistanceIndex() != -1
&& queryMethod.getReturnType().isCollectionLike())) {
NearQueryInteraction near = new NearQueryInteraction(query, queryMethod.getParameters());
return nearQueryMethodContributor(queryMethod, near);
}
if (queryMethod.hasAnnotatedQuery()) {
if (StringUtils.hasText(queryMethod.getAnnotatedQuery())
@ -110,10 +129,6 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -110,10 +129,6 @@ public class MongoRepositoryContributor extends RepositoryContributor {
}
}
if (backoff(queryMethod)) {
return null;
}
if (query.isDelete()) {
return deleteMethodContributor(queryMethod, query);
}
@ -145,7 +160,7 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -145,7 +160,7 @@ public class MongoRepositoryContributor extends RepositoryContributor {
@SuppressWarnings("NullAway")
private QueryInteraction createStringQuery(RepositoryInformation repositoryInformation, MongoQueryMethod queryMethod,
@Nullable Query queryAnnotation, int parameterCount) {
@Nullable Query queryAnnotation) {
QueryInteraction query;
if (queryMethod.hasAnnotatedQuery() && queryAnnotation != null) {
@ -154,7 +169,7 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -154,7 +169,7 @@ public class MongoRepositoryContributor extends RepositoryContributor {
} else {
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
query = new QueryInteraction(queryCreator.createQuery(partTree, parameterCount), partTree.isCountProjection(),
query = new QueryInteraction(queryCreator.createQuery(partTree, queryMethod), partTree.isCountProjection(),
partTree.isDelete(), partTree.isExistsProjection());
}
@ -171,8 +186,8 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -171,8 +186,8 @@ public class MongoRepositoryContributor extends RepositoryContributor {
private static boolean backoff(MongoQueryMethod method) {
// TODO: namedQuery, Regex queries, queries accepting Shapes (e.g. within) or returning arrays.
boolean skip = method.isGeoNearQuery() || method.isSearchQuery()
|| method.getName().toLowerCase(Locale.ROOT).contains("regex") || method.getReturnType().getType().isArray();
boolean skip = method.isSearchQuery() || method.getName().toLowerCase(Locale.ROOT).contains("regex")
|| method.getReturnType().getType().isArray();
if (skip && logger.isDebugEnabled()) {
logger.debug("Skipping AOT generation for [%s]. Method is either returning an array or a geo-near, regex query"
@ -181,22 +196,46 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -181,22 +196,46 @@ public class MongoRepositoryContributor extends RepositoryContributor {
return skip;
}
private static MethodContributor<MongoQueryMethod> aggregationMethodContributor(MongoQueryMethod queryMethod,
private static MethodContributor<MongoQueryMethod> nearQueryMethodContributor(MongoQueryMethod queryMethod,
NearQueryInteraction interaction) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(interaction).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
String variableName = context.localVariable("nearQuery");
builder.add(geoNearBlockBuilder(context, queryMethod).usingQueryVariableName(variableName).build());
if (!context.getBindableParameterNames().isEmpty()) {
String filterQueryVariableName = context.localVariable("filterQuery");
builder.add(queryBlockBuilder(context, queryMethod).usingQueryVariableName(filterQueryVariableName)
.filter(interaction.getQuery()).build());
builder.addStatement("$L.query($L)", variableName, filterQueryVariableName);
}
builder.add(geoNearExecutionBlockBuilder(context, queryMethod).referencing(variableName).build());
return builder.build();
});
}
static MethodContributor<MongoQueryMethod> aggregationMethodContributor(MongoQueryMethod queryMethod,
AggregationInteraction aggregation) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(aggregation).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
String variableName = "aggregation";
builder.add(aggregationBlockBuilder(context, queryMethod).stages(aggregation)
.usingAggregationVariableName("aggregation").build());
builder.add(aggregationExecutionBlockBuilder(context, queryMethod).referencing("aggregation").build());
.usingAggregationVariableName(variableName).build());
builder.add(aggregationExecutionBlockBuilder(context, queryMethod).referencing(variableName).build());
return builder.build();
});
}
private static MethodContributor<MongoQueryMethod> updateMethodContributor(MongoQueryMethod queryMethod,
static MethodContributor<MongoQueryMethod> updateMethodContributor(MongoQueryMethod queryMethod,
UpdateInteraction update) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> {
@ -225,7 +264,7 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -225,7 +264,7 @@ public class MongoRepositoryContributor extends RepositoryContributor {
});
}
private static MethodContributor<MongoQueryMethod> aggregationUpdateMethodContributor(MongoQueryMethod queryMethod,
static MethodContributor<MongoQueryMethod> aggregationUpdateMethodContributor(MongoQueryMethod queryMethod,
AggregationUpdateInteraction update) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> {
@ -251,7 +290,7 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -251,7 +290,7 @@ public class MongoRepositoryContributor extends RepositoryContributor {
});
}
private static MethodContributor<MongoQueryMethod> deleteMethodContributor(MongoQueryMethod queryMethod,
static MethodContributor<MongoQueryMethod> deleteMethodContributor(MongoQueryMethod queryMethod,
QueryInteraction query) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> {
@ -266,7 +305,7 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -266,7 +305,7 @@ public class MongoRepositoryContributor extends RepositoryContributor {
});
}
private static MethodContributor<MongoQueryMethod> queryMethodContributor(MongoQueryMethod queryMethod,
static MethodContributor<MongoQueryMethod> queryMethodContributor(MongoQueryMethod queryMethod,
QueryInteraction query) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> {

68
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/NearQueryInteraction.java

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.data.mongodb.repository.query.MongoParameters;
import org.springframework.data.repository.aot.generate.QueryMetadata;
/**
* An {@link MongoInteraction} to execute a query.
*
* @author Christoph Strobl
* @since 5.0
*/
class NearQueryInteraction extends MongoInteraction implements QueryMetadata {
private final InteractionType interactionType;
private final QueryInteraction query;
private final MongoParameters parameters;
NearQueryInteraction(QueryInteraction query, MongoParameters parameters) {
interactionType = InteractionType.QUERY;
this.query = query;
this.parameters = parameters;
}
@Override
InteractionType getExecutionType() {
return interactionType;
}
public QueryInteraction getQuery() {
return query;
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> serialized = new LinkedHashMap<>();
serialized.put("near", "?%s".formatted(parameters.getNearIndex()));
if (parameters.getRangeIndex() != -1) {
serialized.put("minDistance", "?%s".formatted(parameters.getRangeIndex()));
serialized.put("maxDistance", "?%s".formatted(parameters.getRangeIndex()));
} else if (parameters.getMaxDistanceIndex() != -1) {
serialized.put("minDistance", "?%s".formatted(parameters.getMaxDistanceIndex()));
}
Object filter = query.serialize().get("filter"); // TODO: filter position index can be off due to bindable params
if (filter != null) {
serialized.put("filter", filter);
}
return serialized;
}
}

303
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java

@ -0,0 +1,303 @@ @@ -0,0 +1,303 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.bson.Document;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Polygon;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.Meta;
import org.springframework.data.mongodb.repository.query.MongoParameters.MongoParameter;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution;
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.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName;
import org.springframework.util.ClassUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 5.0
*/
class QueryBlocks {
@NullUnmarked
static class QueryExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private QueryInteraction query;
QueryExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
QueryExecutionCodeBlockBuilder forQuery(QueryInteraction query) {
this.query = query;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
boolean isProjecting = context.getReturnedType().isProjecting();
Class<?> domainType = context.getRepositoryInformation().getDomainType();
Object actualReturnType = queryMethod.getParameters().hasDynamicProjection() || isProjecting
? TypeName.get(context.getActualReturnType().getType())
: domainType;
builder.add("\n");
if (queryMethod.getParameters().hasDynamicProjection()) {
builder.addStatement("$T<$T> $L = $L.query($T.class).as($L)", FindWithQuery.class, actualReturnType,
context.localVariable("finder"), mongoOpsRef, domainType, context.getDynamicProjectionParameterName());
} else if (isProjecting) {
builder.addStatement("$T<$T> $L = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType,
context.localVariable("finder"), mongoOpsRef, domainType, actualReturnType);
} else {
builder.addStatement("$T<$T> $L = $L.query($T.class)", FindWithQuery.class, actualReturnType,
context.localVariable("finder"), mongoOpsRef, domainType);
}
String terminatingMethod;
if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
terminatingMethod = "all()";
} else if (query.isCount()) {
terminatingMethod = "count()";
} else if (query.isExists()) {
terminatingMethod = "exists()";
} else if (queryMethod.isStreamQuery()) {
terminatingMethod = "stream()";
} else {
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()";
}
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()) {
builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class,
context.localVariable("finder"), context.getPageableParameterName(), query.name());
} else if (queryMethod.isScrollQuery()) {
String scrollPositionParameterName = context.getScrollPositionParameterName();
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(),
scrollPositionParameterName);
} else {
if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) {
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
builder.addStatement("return $T.convertNumberToTargetClass($L.matching($L).$L, $T.class)", NumberUtils.class,
context.localVariable("finder"), query.name(), terminatingMethod, returnType);
} else {
builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(),
terminatingMethod);
}
}
return builder.build();
}
}
@NullUnmarked
static class QueryCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private QueryInteraction source;
private final List<CodeBlock> arguments;
private String queryVariableName;
QueryCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.arguments = new ArrayList<>();
this.queryMethod = queryMethod;
collectArguments(context);
}
private void collectArguments(AotQueryMethodGenerationContext context) {
for (MongoParameter parameter : queryMethod.getParameters().getBindableParameters()) {
String parameterName = context.getParameterName(parameter.getIndex());
if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) {
// renders as generic $geometry, thus can be handled by the converter when parsing
arguments.add(CodeBlock.of(parameterName));
} else if (ClassUtils.isAssignable(Circle.class, parameter.getType())
|| ClassUtils.isAssignable(Sphere.class, parameter.getType())) {
// $center | $centerSphere : [ [ <x>, <y> ], <radius> ]
arguments.add(CodeBlock.builder().add(
"$1T.of($1T.of($2L.getCenter().getX(), $2L.getCenter().getY()), $2L.getRadius().getNormalizedValue())",
List.class, parameterName).build());
} else if (ClassUtils.isAssignable(Box.class, parameter.getType())) {
// $box: [ [ <x1>, <y1> ], [ <x2>, <y2> ] ]
arguments.add(CodeBlock.builder().add(
"$1T.of($1T.of($2L.getFirst().getX(), $2L.getFirst().getY()), $1T.of($2L.getSecond().getX(), $2L.getSecond().getY()))",
List.class, parameterName).build());
} else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) {
// $polygon: [ [ <x1> , <y1> ], [ <x2> , <y2> ], [ <x3> , <y3> ], ... ]
String localVar = context.localVariable("_p");
arguments.add(
CodeBlock.builder().add("$1L.getPoints().stream().map($2L -> $3T.of($2L.getX(), $2L.getY())).toList()",
parameterName, localVar, List.class).build());
} else {
arguments.add(CodeBlock.of(parameterName));
}
}
}
QueryCodeBlockBuilder filter(QueryInteraction query) {
this.source = query;
return this;
}
QueryCodeBlockBuilder usingQueryVariableName(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
CodeBlock build() {
Builder builder = CodeBlock.builder();
builder.add("\n");
builder.add(renderExpressionToQuery(source.getQuery().getQueryString(), queryVariableName));
if (StringUtils.hasText(source.getQuery().getFieldsString())) {
builder
.add(MongoCodeBlocks.renderExpressionToDocument(source.getQuery().getFieldsString(), "fields", arguments));
builder.addStatement("$L.setFieldsObject(fields)", queryVariableName);
}
String sortParameter = context.getSortParameterName();
if (StringUtils.hasText(sortParameter)) {
builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
} else if (StringUtils.hasText(source.getQuery().getSortString())) {
builder.add(MongoCodeBlocks.renderExpressionToDocument(source.getQuery().getSortString(), "sort", arguments));
builder.addStatement("$L.setSortObject(sort)", queryVariableName);
}
String limitParameter = context.getLimitParameterName();
if (StringUtils.hasText(limitParameter)) {
builder.addStatement("$L.limit($L)", queryVariableName, limitParameter);
} else if (context.getPageableParameterName() == null && source.getQuery().isLimited()) {
builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit());
}
String pageableParameter = context.getPageableParameterName();
if (StringUtils.hasText(pageableParameter) && !queryMethod.isPageQuery() && !queryMethod.isSliceQuery()) {
builder.addStatement("$L.with($L)", queryVariableName, pageableParameter);
}
MergedAnnotation<Hint> hintAnnotation = context.getAnnotation(Hint.class);
String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null;
if (StringUtils.hasText(hint)) {
builder.addStatement("$L.withHint($S)", queryVariableName, hint);
}
MongoCodeBlocks.appendReadPreference(context, builder, queryVariableName);
MergedAnnotation<Meta> metaAnnotation = context.getAnnotation(Meta.class);
if (metaAnnotation.isPresent()) {
long maxExecutionTimeMs = metaAnnotation.getLong("maxExecutionTimeMs");
if (maxExecutionTimeMs != -1) {
builder.addStatement("$L.maxTimeMsec($L)", queryVariableName, maxExecutionTimeMs);
}
int cursorBatchSize = metaAnnotation.getInt("cursorBatchSize");
if (cursorBatchSize != 0) {
builder.addStatement("$L.cursorBatchSize($L)", queryVariableName, cursorBatchSize);
}
String comment = metaAnnotation.getString("comment");
if (StringUtils.hasText("comment")) {
builder.addStatement("$L.comment($S)", queryVariableName, comment);
}
}
// TODO: Meta annotation: Disk usage
return builder.build();
}
private CodeBlock renderExpressionToQuery(@Nullable String source, String variableName) {
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.addStatement("$1T $2L = new $1T(new $3T())", BasicQuery.class, variableName, Document.class);
} else if (!MongoCodeBlocks.containsPlaceholder(source)) {
builder.addStatement("$1T $2L = new $1T($3T.parse($4S))", BasicQuery.class, variableName, Document.class,
source);
} else {
builder.add("$T $L = createQuery($S, new $T[]{ ", BasicQuery.class, variableName, source, Object.class);
Iterator<CodeBlock> iterator = arguments.iterator();
while (iterator.hasNext()) {
builder.add(iterator.next());
if (iterator.hasNext()) {
builder.add(", ");
} else {
builder.add(" ");
}
}
builder.add("});\n");
}
return builder.build();
}
}
}

145
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateBlocks.java

@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
/*
* Copyright 2025. the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.List;
import java.util.stream.Collectors;
import org.jspecify.annotations.NullUnmarked;
import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicUpdate;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.util.ClassUtils;
import org.springframework.util.NumberUtils;
/**
* @author Christoph Strobl
* @since 2025/06
*/
class UpdateBlocks {
@NullUnmarked
static class UpdateExecutionCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String queryVariableName;
private String updateVariableName;
UpdateExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
UpdateExecutionCodeBlockBuilder withFilter(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
UpdateExecutionCodeBlockBuilder referencingUpdate(String updateVariableName) {
this.updateVariableName = updateVariableName;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
builder.add("\n");
String updateReference = updateVariableName;
Class<?> domainType = context.getRepositoryInformation().getDomainType();
builder.addStatement("$1T<$2T> $3L = $4L.update($2T.class)", ExecutableUpdate.class, domainType,
context.localVariable("updater"), mongoOpsRef);
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
if (ReflectionUtils.isVoid(returnType)) {
builder.addStatement("$L.matching($L).apply($L).all()", context.localVariable("updater"), queryVariableName,
updateReference);
} else if (ClassUtils.isAssignable(Long.class, returnType)) {
builder.addStatement("return $L.matching($L).apply($L).all().getModifiedCount()",
context.localVariable("updater"), queryVariableName, updateReference);
} else {
builder.addStatement("$T $L = $L.matching($L).apply($L).all().getModifiedCount()", Long.class,
context.localVariable("modifiedCount"), context.localVariable("updater"), queryVariableName,
updateReference);
builder.addStatement("return $T.convertNumberToTargetClass($L, $T.class)", NumberUtils.class,
context.localVariable("modifiedCount"), returnType);
}
return builder.build();
}
}
@NullUnmarked
static class UpdateCodeBlockBuilder {
private UpdateInteraction source;
private List<CodeBlock> arguments;
private String updateVariableName;
public UpdateCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.arguments = context.getBindableParameterNames().stream().map(CodeBlock::of).collect(Collectors.toList());
}
public UpdateCodeBlockBuilder update(UpdateInteraction update) {
this.source = update;
return this;
}
public UpdateCodeBlockBuilder usingUpdateVariableName(String updateVariableName) {
this.updateVariableName = updateVariableName;
return this;
}
CodeBlock build() {
Builder builder = CodeBlock.builder();
builder.add("\n");
String tmpVariableName = updateVariableName + "Document";
builder.add(MongoCodeBlocks.renderExpressionToDocument(source.getUpdate().getUpdateString(), tmpVariableName, arguments));
builder.addStatement("$1T $2L = new $1T($3L)", BasicUpdate.class, updateVariableName, tmpVariableName);
return builder.build();
}
}
}

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java

@ -292,7 +292,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter> @@ -292,7 +292,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
*
* @author Oliver Gierke
*/
static class MongoParameter extends Parameter {
public static class MongoParameter extends Parameter {
private final MethodParameter parameter;
private final @Nullable Integer nearIndex;

65
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java

@ -54,6 +54,7 @@ import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; @@ -54,6 +54,7 @@ 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.Streamable;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@ -235,35 +236,7 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -235,35 +236,7 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
return criteria.is(false);
case NEAR:
Range<Distance> range = accessor.getDistanceRange();
Optional<Distance> distance = range.getUpperBound().getValue();
Optional<Distance> minDistance = range.getLowerBound().getValue();
Point point = accessor.getGeoNearLocation();
Point pointToUse = point == null ? nextAs(parameters, Point.class) : point;
boolean isSpherical = isSpherical(property);
return distance.map(it -> {
if (isSpherical || !Metrics.NEUTRAL.equals(it.getMetric())) {
criteria.nearSphere(pointToUse);
} else {
criteria.near(pointToUse);
}
if (pointToUse instanceof GeoJson) { // using GeoJson distance is in meters.
criteria.maxDistance(MetricConversion.getDistanceInMeters(it));
minDistance.map(MetricConversion::getDistanceInMeters).ifPresent(criteria::minDistance);
} else {
criteria.maxDistance(it.getNormalizedValue());
minDistance.map(Distance::getNormalizedValue).ifPresent(criteria::minDistance);
}
return criteria;
}).orElseGet(() -> isSpherical ? criteria.nearSphere(pointToUse) : criteria.near(pointToUse));
return createNearCriteria(property, criteria, parameters);
case WITHIN:
@ -283,6 +256,40 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -283,6 +256,40 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
}
}
@NonNull
private Criteria createNearCriteria(MongoPersistentProperty property, Criteria criteria, Iterator<Object> parameters) {
Range<Distance> range = accessor.getDistanceRange();
Optional<Distance> distance = range.getUpperBound().getValue();
Optional<Distance> minDistance = range.getLowerBound().getValue();
Point point = accessor.getGeoNearLocation();
Point pointToUse = point == null ? nextAs(parameters, Point.class) : point;
boolean isSpherical = isSpherical(property);
return distance.map(it -> {
if (isSpherical || !Metrics.NEUTRAL.equals(it.getMetric())) {
criteria.nearSphere(pointToUse);
} else {
criteria.near(pointToUse);
}
if (pointToUse instanceof GeoJson) { // using GeoJson distance is in meters.
criteria.maxDistance(MetricConversion.getDistanceInMeters(it));
minDistance.map(MetricConversion::getDistanceInMeters).ifPresent(criteria::minDistance);
} else {
criteria.maxDistance(it.getNormalizedValue());
minDistance.map(Distance::getNormalizedValue).ifPresent(criteria::minDistance);
}
return criteria;
}).orElseGet(() -> isSpherical ? criteria.nearSphere(pointToUse) : criteria.near(pointToUse));
}
private boolean isSimpleComparisonPossible(Part part) {
return switch (part.shouldIgnoreCase()) {

23
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java

@ -186,8 +186,11 @@ public interface MongoQueryExecution { @@ -186,8 +186,11 @@ public interface MongoQueryExecution {
return isListOfGeoResult(method.getReturnType()) ? results.getContent() : results;
}
@SuppressWarnings({ "unchecked", "NullAway" })
GeoResults<Object> doExecuteQuery(Query query) {
return doExecuteQuery(nearQuery(query));
}
NearQuery nearQuery(Query query) {
Point nearLocation = accessor.getGeoNearLocation();
Assert.notNull(nearLocation, "[query.location] must not be null");
@ -205,9 +208,12 @@ public interface MongoQueryExecution { @@ -205,9 +208,12 @@ public interface MongoQueryExecution {
distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric()));
Pageable pageable = accessor.getPageable();
nearQuery.with(pageable);
return nearQuery.with(pageable);
}
return (GeoResults<Object>) operation.near(nearQuery).all();
@SuppressWarnings({ "unchecked", "NullAway" })
GeoResults<Object> doExecuteQuery(NearQuery query) {
return (GeoResults<Object>) operation.near(query).all();
}
private static boolean isListOfGeoResult(TypeInformation<?> returnType) {
@ -324,16 +330,11 @@ public interface MongoQueryExecution { @@ -324,16 +330,11 @@ public interface MongoQueryExecution {
@Override
public Object execute(Query query) {
GeoResults<Object> geoResults = doExecuteQuery(query);
NearQuery nearQuery = nearQuery(query);
GeoResults<Object> geoResults = doExecuteQuery(nearQuery);
Page<GeoResult<Object>> page = PageableExecutionUtils.getPage(geoResults.getContent(), accessor.getPageable(),
() -> {
Query countQuery = mongoQuery.createCountQuery(accessor);
countQuery = mongoQuery.applyQueryMetaAttributesWhenPresent(countQuery);
return operation.matching(countQuery).count();
});
() -> operation.near(nearQuery).count());
// transform to GeoPage after applying optimization
return new GeoPage<>(geoResults, accessor.getPageable(), page.getTotalElements());

59
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java

@ -60,6 +60,7 @@ import org.bson.codecs.DecoderContext; @@ -60,6 +60,7 @@ import org.bson.codecs.DecoderContext;
import org.bson.codecs.DocumentCodec;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;
@ -74,6 +75,7 @@ import org.springframework.data.mongodb.CodecRegistryProvider; @@ -74,6 +75,7 @@ import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.core.mapping.FieldName;
import org.springframework.data.mongodb.core.mapping.FieldName.Type;
import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder;
import org.springframework.data.mongodb.core.query.GeoCommand;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -103,7 +105,7 @@ public class BsonUtils { @@ -103,7 +105,7 @@ public class BsonUtils {
public static final Document EMPTY_DOCUMENT = new EmptyDocument();
private static final CodecRegistry JSON_CODEC_REGISTRY = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromCodecs(new PlaceholderCodec()));
MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromProviders(new PlaceholderCodecProvider()));
@SuppressWarnings("unchecked")
@Contract("null, _ -> null")
@ -377,7 +379,7 @@ public class BsonUtils { @@ -377,7 +379,7 @@ public class BsonUtils {
@Contract("null, _ -> !null")
public static BsonValue simpleToBsonValue(@Nullable Object source, CodecRegistry codecRegistry) {
if(source == null) {
if (source == null) {
return BsonNull.VALUE;
}
@ -1031,6 +1033,25 @@ public class BsonUtils { @@ -1031,6 +1033,25 @@ public class BsonUtils {
}
}
@NullUnmarked
public static class PlaceholderCodecProvider implements CodecProvider {
PlaceholderCodec placeholderCodec = new PlaceholderCodec();
GeoCommandCodec geoCommandCodec = new GeoCommandCodec();
@Override
public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
if (ClassUtils.isAssignable(Placeholder.class, clazz)) {
return (Codec<T>) placeholderCodec;
}
if (ClassUtils.isAssignable(GeoCommand.class, clazz)) {
return (Codec<T>) geoCommandCodec;
}
return null;
}
}
/**
* Internal {@link Codec} implementation to write
* {@link org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder placeholders}.
@ -1060,4 +1081,38 @@ public class BsonUtils { @@ -1060,4 +1081,38 @@ public class BsonUtils {
return Placeholder.class;
}
}
static class GeoCommandCodec implements Codec<GeoCommand> {
@Override
public GeoCommand decode(BsonReader reader, DecoderContext decoderContext) {
return null;
}
@Override
public void encode(BsonWriter writer, GeoCommand value, EncoderContext encoderContext) {
if (writer instanceof SpringJsonWriter sjw) {
if (!value.getCommand().equals("$geometry")) {
writer.writeStartDocument();
writer.writeName(value.getCommand());
if (value.getShape() instanceof Placeholder p) { // maybe we should wrap input to use geo command object
sjw.writePlaceholder(p.toString());
}
writer.writeEndDocument();
} else {
if (value.getShape() instanceof Placeholder p) { // maybe we should wrap input to use geo command object
sjw.writePlaceholder(p.toString());
}
}
} else {
writer.writeString(value.getCommand(), value.getShape().toString());
}
}
@Override
public Class<GeoCommand> getEncoderClass() {
return null;
}
}
}

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java

@ -463,7 +463,7 @@ class SpringJsonWriter implements BsonWriter { @@ -463,7 +463,7 @@ class SpringJsonWriter implements BsonWriter {
write(placeholder);
}
private void write(String str) {
public void write(String str) {
buffer.append(str);
}

26
spring-data-mongodb/src/test/java/example/aot/Location.java

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.aot;
import org.springframework.data.geo.Point;
/**
* @param planet
* @param coordinates
* @author Christoph Strobl
*/
public record Location(String planet, Point coordinates) {
}

10
spring-data-mongodb/src/test/java/example/aot/User.java

@ -32,6 +32,8 @@ public class User { @@ -32,6 +32,8 @@ public class User {
@Field("last_name") String lastname;
Location location;
Instant registrationDate;
Instant lastSeen;
Long visits;
@ -91,4 +93,12 @@ public class User { @@ -91,4 +93,12 @@ public class User {
public void setVisits(Long visits) {
this.visits = visits;
}
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
}

38
spring-data-mongodb/src/test/java/example/aot/UserRepository.java

@ -28,13 +28,26 @@ import org.springframework.data.annotation.Id; @@ -28,13 +28,26 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoPage;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.Person;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.Update;
@ -103,7 +116,30 @@ public interface UserRepository extends CrudRepository<User, String> { @@ -103,7 +116,30 @@ public interface UserRepository extends CrudRepository<User, String> {
Window<User> findTop2WindowByLastnameStartingWithOrderByUsername(String lastname, ScrollPosition scrollPosition);
// TODO: GeoQueries
List<User> findByLocationCoordinatesNear(Point location);
List<User> findByLocationCoordinatesWithin(Circle circle);
List<User> findByLocationCoordinatesWithin(Sphere circle);
List<User> findByLocationCoordinatesWithin(Box box);
List<User> findByLocationCoordinatesWithin(Polygon polygon);
List<User> findByLocationCoordinatesWithin(GeoJsonPolygon polygon);
List<User> findUserByLocationCoordinatesWithin(GeoJson<?> geoJson);
GeoResults<User> findByLocationCoordinatesNear(Point point, Distance maxDistance);
GeoResults<User> findByLocationCoordinatesNearAndLastname(Point point, Distance maxDistance, String lastname);
List<GeoResult<User>> findUserAsListByLocationCoordinatesNear(Point point, Distance maxDistance);
GeoResults<User> findByLocationCoordinatesNear(Point point, Range<Distance> distance);
GeoPage<User> findByLocationCoordinatesNear(Point point, Distance maxDistance, Pageable pageable);
// TODO: TextSearch
/* Annotated Queries */

24
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java

@ -15,10 +15,13 @@ @@ -15,10 +15,13 @@
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.data.mongodb.test.util.DirtiesStateExtension.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
import static org.springframework.data.mongodb.test.util.DirtiesStateExtension.DirtiesState;
import static org.springframework.data.mongodb.test.util.DirtiesStateExtension.StateFunctions;
import java.util.Date;
import java.util.List;
@ -47,6 +50,7 @@ import org.springframework.data.mongodb.core.mapping.DocumentReference; @@ -47,6 +50,7 @@ import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.test.util.DirtiesStateExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
import org.springframework.data.mongodb.test.util.Template;
@ -80,7 +84,7 @@ class ExecutableFindOperationSupportTests implements StateFunctions { @@ -80,7 +84,7 @@ class ExecutableFindOperationSupportTests implements StateFunctions {
@Override
public void setupState() {
template.indexOps(Planet.class).ensureIndex(
template.indexOps(Planet.class).createIndex(
new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx"));
initPersons();
@ -161,7 +165,7 @@ class ExecutableFindOperationSupportTests implements StateFunctions { @@ -161,7 +165,7 @@ class ExecutableFindOperationSupportTests implements StateFunctions {
void findAllAsDocument() {
assertThat(
template.query(Document.class).inCollection(STAR_WARS).matching(query(where("firstname").is("luke"))).all())
.hasSize(1);
.hasSize(1);
}
@Test // DATAMONGO-1563
@ -323,6 +327,14 @@ class ExecutableFindOperationSupportTests implements StateFunctions { @@ -323,6 +327,14 @@ class ExecutableFindOperationSupportTests implements StateFunctions {
assertThat(results.getContent().get(0).getDistance()).isNotNull();
}
@Test
void countResultsOfNearQuery() {
Long count = template.query(Planet.class)
.near(NearQuery.near(-73.9667, 40.78).spherical(true).query(new Query(where("name").is("alderan")))).count();
assertThat(count).isEqualTo(1);
}
@Test // DATAMONGO-1563
void findAllNearByWithCollectionAndProjection() {

71
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java

@ -15,10 +15,12 @@ @@ -15,10 +15,12 @@
*/
package org.springframework.data.mongodb.repository;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import static org.springframework.data.geo.Metrics.*;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.springframework.data.geo.Metrics.KILOMETERS;
import java.util.ArrayList;
import java.util.Arrays;
@ -38,13 +40,22 @@ import org.bson.Document; @@ -38,13 +40,22 @@ import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.*;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.GenericPropertyMatcher;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Window;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
@ -216,8 +227,8 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -216,8 +227,8 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
@Test // GH-4397
void appliesLimitToScrollingCorrectly() {
Window<Person> page = repository.findByLastnameLikeOrderByLastnameAscFirstnameAsc("*a*",
ScrollPosition.keyset(), Limit.of(2));
Window<Person> page = repository.findByLastnameLikeOrderByLastnameAscFirstnameAsc("*a*", ScrollPosition.keyset(),
Limit.of(2));
assertThat(page.isLast()).isFalse();
assertThat(page.size()).isEqualTo(2);
@ -250,7 +261,8 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -250,7 +261,8 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
@Test // GH-4397
void executesFinderCorrectlyWithSortAndLimit() {
List<Person> page = repository.findByLastnameLike("*a*", Sort.by(Direction.ASC, "lastname", "firstname"), Limit.of(2));
List<Person> page = repository.findByLastnameLike("*a*", Sort.by(Direction.ASC, "lastname", "firstname"),
Limit.of(2));
assertThat(page).containsExactly(carter, stefan);
}
@ -462,6 +474,22 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -462,6 +474,22 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
assertThat(results.getContent()).isNotEmpty();
}
@Test
void executesGeoNearQueryWithAdditionalFilterCorrectly() {
Point point = new Point(-73.99171, 40.738868);
dave.setLocation(point);
repository.save(dave);
Person p2 = new Person("fn", "ln", 42, Sex.MALE);
p2.setLocation(point);
repository.save(p2);
GeoResults<Person> results = repository.findByLocationNearAndLastname(new Point(-73.99, 40.73),
Distance.of(2000, Metrics.KILOMETERS), "ln");
assertThat(results.getContent()).hasSize(1);
}
@Test
void executesGeoPageQueryForResultsCorrectly() {
@ -638,6 +666,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -638,6 +666,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
assertThat(results.getContent()).isNotEmpty();
assertThat(results.getNumberOfElements()).isEqualTo(2);
assertThat(results.getTotalElements()).isEqualTo(5);
assertThat(results.isFirst()).isFalse();
assertThat(results.isLast()).isFalse();
assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
@ -697,6 +726,30 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -697,6 +726,30 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS);
}
@Test
void executesGeoPageCountCorrectly() {
Point farAway = new Point(-73.9, 40.7);
Point here = new Point(-73.99, 40.73);
dave.setLocation(farAway);
oliver.setLocation(here);
carter.setLocation(here);
boyd.setLocation(here);
leroi.setLocation(here);
repository.saveAll(Arrays.asList(dave, oliver, carter, boyd, leroi));
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
Distance.of(5, Metrics.KILOMETERS), PageRequest.of(1, 2));
assertThat(results.getContent()).isNotEmpty();
assertThat(results.getNumberOfElements()).isEqualTo(2);
assertThat(results.getTotalElements()).isEqualTo(4);
assertThat(results.isFirst()).isFalse();
assertThat(results.isLast()).isTrue();
}
@Test // DATAMONGO-1608
void findByFirstNameIgnoreCaseWithNull() {

2
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java

@ -222,6 +222,8 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query @@ -222,6 +222,8 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
GeoResults<Person> findByLocationNear(Point point, Distance maxDistance);
GeoResults<Person> findByLocationNearAndLastname(Point point, Distance maxDistance, String Lastname);
// DATAMONGO-1110
GeoResults<Person> findPersonByLocationNear(Point point, Range<Distance> distance);

9
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java

@ -93,7 +93,7 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce @@ -93,7 +93,7 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce
Method target = ReflectionUtils.findMethod(fragment.getClass(), method.getName(), method.getParameterTypes());
if (target == null) {
throw new NoSuchMethodException("Method [%s] is not implemented by [%s]".formatted(method, target));
throw new MethodNotImplementedException("Method [%s] is not implemented by [%s]".formatted(method, target));
}
try {
@ -127,4 +127,11 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce @@ -127,4 +127,11 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce
}
};
}
public static class MethodNotImplementedException extends RuntimeException {
public MethodNotImplementedException(String message) {
super(message);
}
}
}

154
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java

@ -15,7 +15,9 @@ @@ -15,7 +15,9 @@
*/
package org.springframework.data.mongodb.repository.aot;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import example.aot.User;
import example.aot.UserProjection;
@ -27,9 +29,9 @@ import java.util.List; @@ -27,9 +29,9 @@ import java.util.List;
import java.util.Optional;
import org.bson.Document;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -38,19 +40,32 @@ import org.springframework.data.domain.Limit; @@ -38,19 +40,32 @@ import org.springframework.data.domain.Limit;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoPage;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoTestUtils;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.StringUtils;
import com.mongodb.client.MongoClient;
import com.mongodb.client.model.IndexOptions;
/**
* Integration tests for the {@link UserRepository} AOT fragment.
@ -80,6 +95,12 @@ class MongoRepositoryContributorTests { @@ -80,6 +95,12 @@ class MongoRepositoryContributorTests {
}
}
@BeforeAll
static void beforeAll() {
client.getDatabase(DB_NAME).getCollection("user").createIndex(new Document("location.coordinates", "2d"),
new IndexOptions());
}
@BeforeEach
void beforeEach() {
@ -590,6 +611,123 @@ class MongoRepositoryContributorTests { @@ -590,6 +611,123 @@ class MongoRepositoryContributorTests {
.withMessageContaining("'locale' is invalid");
}
@Test // GH-5004
void testNear() {
List<User> users = fragment.findByLocationCoordinatesNear(new Point(-73.99171, 40.738868));
assertThat(users).extracting(User::getUsername).containsExactly("leia", "vader");
}
@Test // GH-5004
void testNearWithGeoJson() {
List<User> users = fragment.findByLocationCoordinatesNear(new GeoJsonPoint(-73.99171, 40.738868));
assertThat(users).extracting(User::getUsername).containsExactly("leia", "vader");
}
@Test // GH-5004
void testGeoWithinCircle() {
List<User> users = fragment.findByLocationCoordinatesWithin(new Circle(-78.99171, 45.738868, 170));
assertThat(users).extracting(User::getUsername).containsExactly("leia", "vader");
}
@Test // GH-5004
void testWithinBox() {
Box box = new Box(new Point(-78.99171, 35.738868), new Point(-68.99171, 45.738868));
List<User> result = fragment.findByLocationCoordinatesWithin(box);
assertThat(result).extracting(User::getUsername).containsExactly("leia", "vader");
}
@Test // GH-5004
void findsPeopleByLocationWithinPolygon() {
Point first = new Point(-78.99171, 35.738868);
Point second = new Point(-78.99171, 45.738868);
Point third = new Point(-68.99171, 45.738868);
Point fourth = new Point(-68.99171, 35.738868);
List<User> result = fragment.findByLocationCoordinatesWithin(new Polygon(first, second, third, fourth));
assertThat(result).extracting(User::getUsername).containsExactly("leia", "vader");
}
@Test // GH-5004
void findsPeopleByLocationWithinGeoJsonPolygon() {
Point first = new Point(-78.99171, 35.738868);
Point second = new Point(-78.99171, 45.738868);
Point third = new Point(-68.99171, 45.738868);
Point fourth = new Point(-68.99171, 35.738868);
List<User> result = fragment
.findByLocationCoordinatesWithin(new GeoJsonPolygon(first, second, third, fourth, first));
assertThat(result).extracting(User::getUsername).containsExactly("leia", "vader");
}
@Test // GH-5004
void findsPeopleByLocationWithinSomeGenericGeoJsonObject() {
Point first = new Point(-78.99171, 35.738868);
Point second = new Point(-78.99171, 45.738868);
Point third = new Point(-68.99171, 45.738868);
Point fourth = new Point(-68.99171, 35.738868);
List<User> result = fragment
.findUserByLocationCoordinatesWithin(new GeoJsonPolygon(first, second, third, fourth, first));
assertThat(result).extracting(User::getUsername).containsExactly("leia", "vader");
}
@Test // GH-5004
void testNearWithGeoResult() {
GeoResults<User> users = fragment.findByLocationCoordinatesNear(new Point(-73.99, 40.73),
Distance.of(5, Metrics.KILOMETERS));
assertThat(users).extracting(GeoResult::getContent).extracting(User::getUsername).containsExactly("leia");
}
@Test // GH-5004
void testNearWithAdditionalFilterQueryAsGeoResult() {
GeoResults<User> users = fragment.findByLocationCoordinatesNearAndLastname(new Point(-73.99, 40.73),
Distance.of(50, Metrics.KILOMETERS), "Organa");
assertThat(users).extracting(GeoResult::getContent).extracting(User::getUsername).containsExactly("leia");
}
@Test // GH-5004
void testNearReturningListOfGeoResult() {
List<GeoResult<User>> users = fragment.findUserAsListByLocationCoordinatesNear(new Point(-73.99, 40.73),
Distance.of(5, Metrics.KILOMETERS));
assertThat(users).extracting(GeoResult::getContent).extracting(User::getUsername).containsExactly("leia");
}
@Test // GH-5004
void testNearWithRange() {
Range<Distance> range = Distance.between(Distance.of(5, Metrics.KILOMETERS), Distance.of(2000, Metrics.KILOMETERS));
GeoResults<User> users = fragment.findByLocationCoordinatesNear(new Point(-73.99, 40.73), range);
assertThat(users).extracting(GeoResult::getContent).extracting(User::getUsername).containsExactly("vader");
}
@Test // GH-5004
void testNearReturningGeoPage() {
GeoPage<User> page1 = fragment.findByLocationCoordinatesNear(new Point(-73.99, 40.73),
Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(0, 1));
assertThat(page1.hasNext()).isTrue();
GeoPage<User> page2 = fragment.findByLocationCoordinatesNear(new Point(-73.99, 40.73),
Distance.of(2000, Metrics.KILOMETERS), page1.nextPageable());
assertThat(page2.hasNext()).isFalse();
}
/**
* GeoResults<Person> results = repository.findPersonByLocationNear(new Point(-73.99, 40.73), range);
*/
private static void initUsers() {
Document luke = Document.parse("""
@ -619,6 +757,12 @@ class MongoRepositoryContributorTests { @@ -619,6 +757,12 @@ class MongoRepositoryContributorTests {
"username": "leia",
"first_name": "Leia",
"last_name": "Organa",
"location" : {
"planet" : "Coruscant",
"coordinates" : {
"x" : -73.99171, "y" : 40.738868
}
},
"_class": "example.springdata.aot.User"
}""");
@ -677,6 +821,12 @@ class MongoRepositoryContributorTests { @@ -677,6 +821,12 @@ class MongoRepositoryContributorTests {
"username": "vader",
"first_name": "Anakin",
"last_name": "Skywalker",
"location" : {
"planet" : "Death Star",
"coordinates" : {
"x" : -73.9, "y" : 40.7
}
},
"visits" : 50,
"posts": [
{

7
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java

@ -15,9 +15,9 @@ @@ -15,9 +15,9 @@
*/
package org.springframework.data.mongodb.repository.aot;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import example.aot.UserRepository;
@ -27,7 +27,6 @@ import java.util.List; @@ -27,7 +27,6 @@ import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

222
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/QueryMethodContributionUnitTests.java

@ -0,0 +1,222 @@ @@ -0,0 +1,222 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.aot;
import static org.assertj.core.api.Assertions.assertThat;
import example.aot.User;
import example.aot.UserRepository;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata;
import org.springframework.data.repository.aot.generate.MethodContributor;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.MethodSpec;
/**
* @author Christoph Strobl
*/
public class QueryMethodContributionUnitTests {
@Test // GH-5004
void rendersQueryForNearUsingPoint() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesNear", Point.class);
assertThat(methodSpec.toString()) //
.contains("{'location.coordinates':{'$near':?0}}") //
.contains("Object[]{ location }") //
.contains("return finder.matching(filterQuery).all()");
}
@Test // GH-5004
void rendersQueryForWithinUsingCircle() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesWithin", Circle.class);
assertThat(methodSpec.toString()) //
.contains("{'location.coordinates':{'$geoWithin':{'$center':?0}}") //
.contains(
"List.of(circle.getCenter().getX(), circle.getCenter().getY()), circle.getRadius().getNormalizedValue())") //
.contains("return finder.matching(filterQuery).all()");
}
@Test // GH-5004
void rendersQueryForWithinUsingSphere() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesWithin", Sphere.class);
assertThat(methodSpec.toString()) //
.contains("{'location.coordinates':{'$geoWithin':{'$centerSphere':?0}}") //
.contains(
"List.of(circle.getCenter().getX(), circle.getCenter().getY()), circle.getRadius().getNormalizedValue())") //
.contains("return finder.matching(filterQuery).all()");
}
@Test // GH-5004
void rendersQueryForWithinUsingBox() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesWithin", Box.class);
assertThat(methodSpec.toString()) //
.contains("{'location.coordinates':{'$geoWithin':{'$box':?0}}") //
.contains("List.of(box.getFirst().getX(), box.getFirst().getY())") //
.contains("List.of(box.getSecond().getX(), box.getSecond().getY())") //
.contains("return finder.matching(filterQuery).all()");
}
@Test // GH-5004
void rendersQueryForWithinUsingPolygon() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesWithin", Polygon.class);
assertThat(methodSpec.toString()) //
.contains("{'location.coordinates':{'$geoWithin':{'$polygon':?0}}") //
.contains("polygon.getPoints().stream().map(_p ->") //
.contains("List.of(_p.getX(), _p.getY())") //
.contains("return finder.matching(filterQuery).all()");
}
@Test // GH-5004
void rendersQueryForWithinUsingGeoJsonPolygon() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesWithin", GeoJsonPolygon.class);
assertThat(methodSpec.toString()) //
.contains("{'location.coordinates':{'$geoWithin':{'$geometry':?0}}") //
.contains("Object[]{ polygon }") //
.contains("return finder.matching(filterQuery).all()");
}
@Test // GH-5004
void rendersNearQueryForGeoResults() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepoWithMeta.class, "findByLocationCoordinatesNear", Point.class,
Distance.class);
assertThat(methodSpec.toString()) //
.contains("NearQuery.near(point)") //
.contains("nearQuery.maxDistance(maxDistance).in(maxDistance.getMetric())") //
.contains(".withReadPreference(com.mongodb.ReadPreference.valueOf(\"NEAREST\")") //
.doesNotContain("nearQuery.query(") //
.contains(".near(nearQuery)") //
.contains("return nearFinder.all()");
}
@Test // GH-5004
void rendersNearQueryWithDistanceRangeForGeoResults() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesNear", Point.class, Range.class);
assertThat(methodSpec.toString()) //
.contains("NearQuery.near(point)") //
.contains("if(distance.getLowerBound().isBounded())") //
.contains("nearQuery.minDistance(min).in(min.getMetric())") //
.contains("if(distance.getUpperBound().isBounded())") //
.contains("nearQuery.maxDistance(max).in(max.getMetric())") //
.contains(".near(nearQuery)") //
.contains("return nearFinder.all()");
}
@Test // GH-5004
void rendersNearQueryReturningGeoPage() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesNear", Point.class, Distance.class,
Pageable.class);
assertThat(methodSpec.toString()) //
.contains("NearQuery.near(point)") //
.contains("nearQuery.maxDistance(maxDistance).in(maxDistance.getMetric())") //
.doesNotContain("nearQuery.query(") //
.contains("var geoResult = nearFinder.all()") //
.contains("PageableExecutionUtils.getPage(geoResult.getContent(), pageable, () -> nearFinder.count())")
.contains("GeoPage<>(geoResult, pageable, resultPage.getTotalElements())");
}
@Test // GH-5004
void rendersNearQueryWithFilterForGeoResults() throws NoSuchMethodException {
MethodSpec methodSpec = codeOf(UserRepository.class, "findByLocationCoordinatesNearAndLastname", Point.class,
Distance.class, String.class);
assertThat(methodSpec.toString()) //
.contains("NearQuery.near(point)") //
.contains("nearQuery.maxDistance(maxDistance).in(maxDistance.getMetric())") //
.contains("filterQuery = createQuery(\"{'lastname':?0}\", new java.lang.Object[]{ lastname })") //
.contains("nearQuery.query(filterQuery)") //
.contains(".near(nearQuery)") //
.contains("return nearFinder.all()");
}
private static MethodSpec codeOf(Class<?> repository, String methodName, Class<?>... args)
throws NoSuchMethodException {
Method method = repository.getMethod(methodName, args);
TestMongoAotRepositoryContext repoContext = new TestMongoAotRepositoryContext(repository, null);
MongoRepositoryContributor contributor = new MongoRepositoryContributor(repoContext);
MethodContributor<? extends QueryMethod> methodContributor = contributor.contributeQueryMethod(method);
if (methodContributor == null) {
Assertions.fail("No contribution for method %s.%s(%s)".formatted(repository.getSimpleName(), methodName,
Arrays.stream(args).map(Class::getSimpleName).toList()));
}
AotRepositoryFragmentMetadata metadata = new AotRepositoryFragmentMetadata(ClassName.get(UserRepository.class));
metadata.addField(
FieldSpec.builder(MongoOperations.class, "mongoOperations", Modifier.PRIVATE, Modifier.FINAL).build());
TestQueryMethodGenerationContext methodContext = new TestQueryMethodGenerationContext(
repoContext.getRepositoryInformation(), method, methodContributor.getQueryMethod(), metadata);
return methodContributor.contribute(methodContext);
}
static class TestQueryMethodGenerationContext extends AotQueryMethodGenerationContext {
protected TestQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method,
QueryMethod queryMethod, AotRepositoryFragmentMetadata targetTypeMetadata) {
super(repositoryInformation, method, queryMethod, targetTypeMetadata);
}
}
interface UserRepoWithMeta extends Repository<User, String> {
@ReadPreference("NEAREST")
GeoResults<User> findByLocationCoordinatesNear(Point point, Distance maxDistance);
}
}

4
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java

@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test; @@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.geo.Distance;
@ -171,7 +170,6 @@ class MongoQueryExecutionUnitTests { @@ -171,7 +170,6 @@ class MongoQueryExecutionUnitTests {
when(mongoOperationsMock.query(any(Class.class))).thenReturn(findOperationMock);
when(findOperationMock.near(any(NearQuery.class))).thenReturn(terminatingGeoMock);
doReturn(new GeoResults<>(Collections.emptyList())).when(terminatingGeoMock).all();
doReturn(terminatingMock).when(findOperationMock).matching(any(Query.class));
ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter,
new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(2, 10) }));
@ -183,7 +181,7 @@ class MongoQueryExecutionUnitTests { @@ -183,7 +181,7 @@ class MongoQueryExecutionUnitTests {
execution.execute(new Query());
verify(terminatingGeoMock).all();
verify(terminatingMock).count();
verify(terminatingGeoMock).count();
}
@Test // DATAMONGO-2351

3
spring-data-mongodb/src/test/resources/logback.xml

@ -20,8 +20,9 @@ @@ -20,8 +20,9 @@
<logger name="org.springframework.data.mongodb.test.util" level="info"/>
<!-- AOT Code Generation -->
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="warn" />
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="trace" />
<logger name="org.springframework.data.mongodb.core.MongoTemplate" level="debug"/>
<root level="error">
<appender-ref ref="console" />
</root>

32
src/main/antora/modules/ROOT/pages/mongodb/repositories/query-methods.adoc

@ -209,9 +209,9 @@ NOTE: If the property criterion compares a document, the order of the fields and @@ -209,9 +209,9 @@ NOTE: If the property criterion compares a document, the order of the fields and
== Geo-spatial Queries
As you saw in the preceding table of keywords, a few keywords trigger geo-spatial operations within a MongoDB query.
The `Near` keyword allows some further modification, as the next few examples show.
The `Near` and `Within` keywords allows some further modification, as the next few examples show.
The following example shows how to define a `near` query that finds all persons with a given distance of a given point:
The following example shows how to define a `near` / `within` query that finds all persons using different shapes:
.Advanced `Near` queries
[tabs]
@ -222,8 +222,20 @@ Imperative:: @@ -222,8 +222,20 @@ Imperative::
----
public interface PersonRepository extends MongoRepository<Person, String> {
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance } }
List<Person> findByLocationNear(Point location, Distance distance);
// { 'location' : { $geoWithin: { $center: [ [ circle.center.x, circle.center.y ], circle.radius ] } } }
List<Person> findByLocationWithin(Circle circle);
// { 'location' : { $geoWithin: { $box: [ [ box.first.x, box.first.y ], [ box.second.x, box.second.y ] ] } } }
List<Person> findByLocationWithin(Box box);
// { 'location' : { $geoWithin: { $polygon: [ [ polygon.x1, polygon.y1 ], [ polygon.x2, polygon.y2 ], ... ] } } }
List<Person> findByLocationWithin(Polygon polygon);
// { 'location' : { $geoWithin: { $geometry: { $type : 'polygon', coordinates: [[ polygon.x1, polygon.y1 ], [ polygon.x2, polygon.y2 ], ... ] } } } }
List<Person> findByLocationWithin(GeoJsonPolygon polygon);
}
----
@ -233,8 +245,20 @@ Reactive:: @@ -233,8 +245,20 @@ Reactive::
----
interface PersonRepository extends ReactiveMongoRepository<Person, String> {
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance } }
Flux<Person> findByLocationNear(Point location, Distance distance);
// { 'location' : { $geoWithin: { $center: [ [ circle.center.x, circle.center.y ], circle.radius ] } } }
Flux<Person> findByLocationWithin(Circle circle);
// { 'location' : { $geoWithin: { $box: [ [ box.first.x, box.first.y ], [ box.second.x, box.second.y ] ] } } }
Flux<Person> findByLocationWithin(Box box);
// { 'location' : { $geoWithin: { $polygon: [ [ polygon.x1, polygon.y1 ], [ polygon.x2, polygon.y2 ], ... ] } } }
Flux<Person> findByLocationWithin(Polygon polygon);
// { 'location' : { $geoWithin: { $geometry: { $type : 'polygon', coordinates: [[ polygon.x1, polygon.y1 ], [ polygon.x2, polygon.y2 ], ... ] } } } }
Flux<Person> findByLocationWithin(GeoJsonPolygon polygon);
}
----
======

Loading…
Cancel
Save