Browse Source

Extend AOT Repository Support.

- Introduce AOT fragment base class.
- Refactor Delete execution to be reusable.
- Add support for updates.
- Add support for aggregations.
- Move types to repository package.
- Update documentation.

See: #4939
pull/4976/head
Christoph Strobl 9 months ago committed by Mark Paluch
parent
commit
6c6438ec21
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 315
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java
  2. 136
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java
  3. 46
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
  4. 28
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
  5. 56
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java
  6. 52
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java
  7. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java
  8. 42
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java
  9. 147
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java
  10. 777
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java
  11. 62
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java
  12. 286
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java
  13. 83
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java
  14. 26
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java
  15. 51
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java
  16. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java
  17. 58
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java
  18. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
  19. 35
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
  20. 77
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java
  21. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
  22. 44
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java
  23. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java
  24. 28
      spring-data-mongodb/src/test/java/example/aot/User.java
  25. 1
      spring-data-mongodb/src/test/java/example/aot/UserProjection.java
  26. 142
      spring-data-mongodb/src/test/java/example/aot/UserRepository.java
  27. 18
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java
  28. 662
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java
  29. 127
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java
  30. 650
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java
  31. 39
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java
  32. 27
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java
  33. 14
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java
  34. 37
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java
  35. 8
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java
  36. 4
      spring-data-mongodb/src/test/resources/logback.xml
  37. 1
      src/main/antora/modules/ROOT/nav.adoc
  38. 88
      src/main/antora/modules/ROOT/pages/mongodb/aot.adoc

315
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java

@ -1,315 +0,0 @@ @@ -1,315 +0,0 @@
/*
* 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.aot.generated;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.mongodb.BindableMongoExpression;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX.Type;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution;
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
*/
public class MongoBlocks {
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
static QueryBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
return new QueryBlockBuilder(context, queryMethod);
}
static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new QueryExecutionBlockBuilder(context, queryMethod);
}
static DeleteExecutionBuilder deleteExecutionBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new DeleteExecutionBuilder(context, queryMethod);
}
static class DeleteExecutionBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
String queryVariableName;
public DeleteExecutionBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
public DeleteExecutionBuilder referencing(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
public CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
boolean isProjecting = context.getActualReturnType() != null
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
context.getActualReturnType());
Object actualReturnType = isProjecting ? context.getActualReturnType().getType()
: context.getRepositoryInformation().getDomainType();
builder.add("\n");
builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class,
context.getRepositoryInformation().getDomainType(),
mongoOpsRef, context.getRepositoryInformation().getDomainType());
Type type = Type.FIND_AND_REMOVE_ALL;
if (!queryMethod.isCollectionQuery()) {
if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) {
type = Type.FIND_AND_REMOVE_ONE;
} else {
type = Type.ALL;
}
}
actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())
? ClassName.get(context.getMethod().getReturnType())
: queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType;
builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecutionX.class,
DeleteExecutionX.Type.class, type.name(), queryVariableName);
return builder.build();
}
}
static class QueryExecutionBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private String queryVariableName;
private boolean count, exists;
public QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.queryMethod = queryMethod;
}
QueryExecutionBlockBuilder referencing(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
QueryExecutionBlockBuilder count(boolean count) {
this.count = count;
return this;
}
QueryExecutionBlockBuilder exists(boolean exists) {
this.exists = exists;
return this;
}
CodeBlock build() {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
Builder builder = CodeBlock.builder();
boolean isProjecting = context.getReturnedType().isProjecting();
Object actualReturnType = isProjecting ? context.getActualReturnType().getType()
: context.getRepositoryInformation().getDomainType();
builder.add("\n");
if (isProjecting) {
builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType,
mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType);
} else {
builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef,
context.getRepositoryInformation().getDomainType());
}
String terminatingMethod;
if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
terminatingMethod = "all()";
} else if (count) {
terminatingMethod = "count()";
} else if (exists) {
terminatingMethod = "exists()";
} else {
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()";
}
if (queryMethod.isPageQuery()) {
builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class,
context.getPageableParameterName(), queryVariableName);
} else if (queryMethod.isSliceQuery()) {
builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class,
context.getPageableParameterName(), queryVariableName);
} else {
builder.addStatement("return finder.matching($L).$L", queryVariableName, terminatingMethod);
}
return builder.build();
}
}
static class QueryBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
StringQuery source;
List<String> arguments;
private String queryVariableName;
public QueryBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) {
this.context = context;
this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName)
.collect(Collectors.toList());
// ParametersSource parametersSource = ParametersSource.of(repositoryInformation, metadata.getRepositoryMethod());
// this.argumentSource = new MongoParameters(parametersSource, false);
this.queryMethod = queryMethod;
}
public QueryBlockBuilder filter(StringQuery query) {
this.source = query;
return this;
}
public QueryBlockBuilder usingQueryVariableName(String queryVariableName) {
this.queryVariableName = queryVariableName;
return this;
}
CodeBlock build() {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
String queryDocumentVariableName = "%sDocument".formatted(queryVariableName);
builder.add(renderExpressionToDocument(source.getQueryString(), queryVariableName));
builder.addStatement("$T $L = new $T($L)", BasicQuery.class, queryVariableName, BasicQuery.class,
queryDocumentVariableName);
if (StringUtils.hasText(source.getFieldsString())) {
builder.add(renderExpressionToDocument(source.getFieldsString(), "fields"));
builder.addStatement("$L.setFieldsObject(fieldsDocument)", queryVariableName);
}
String sortParameter = context.getSortParameterName();
if (StringUtils.hasText(sortParameter)) {
builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
} else if (StringUtils.hasText(source.getSortString())) {
builder.add(renderExpressionToDocument(source.getSortString(), "sort"));
builder.addStatement("$L.setSortObject(sortDocument)", queryVariableName);
}
String limitParameter = context.getLimitParameterName();
if (StringUtils.hasText(limitParameter)) {
builder.addStatement("$L.limit($L)", queryVariableName, limitParameter);
} else if (context.getPageableParameterName() == null && source.isLimited()) {
builder.addStatement("$L.limit($L)", queryVariableName, source.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);
}
// TODO: all the meta stuff
return builder.build();
}
private CodeBlock renderExpressionToDocument(@Nullable String source, String variableName) {
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.addStatement("$T $L = new $T()", Document.class, "%sDocument".formatted(variableName), Document.class);
} else if (!containsPlaceholder(source)) {
builder.addStatement("$T $L = $T.parse($S)", Document.class, "%sDocument".formatted(variableName),
Document.class, source);
} else {
String mongoOpsRef = context.fieldNameOf(MongoOperations.class);
String tmpVarName = "%sString".formatted(variableName);
builder.addStatement("String $L = $S", tmpVarName, source);
builder.addStatement("$T $L = new $T($L, $L.getConverter(), new $T[]{ $L }).toDocument()", Document.class,
"%sDocument".formatted(variableName), BindableMongoExpression.class, tmpVarName, mongoOpsRef, Object.class,
StringUtils.collectionToDelimitedString(arguments, ", "));
}
return builder.build();
}
private boolean containsPlaceholder(String source) {
return PARAMETER_BINDING_PATTERN.matcher(source).find();
}
}
}

136
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java

@ -1,136 +0,0 @@ @@ -1,136 +0,0 @@
/*
* 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.aot.generated;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.mongodb.aot.generated.MongoBlocks.QueryBlockBuilder;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
import org.springframework.data.repository.aot.generate.MethodContributor;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.TypeName;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 2025/01
*/
public class MongoRepositoryContributor extends RepositoryContributor {
private final AotQueryCreator queryCreator;
private final MongoMappingContext mappingContext;
public MongoRepositoryContributor(AotRepositoryContext repositoryContext) {
super(repositoryContext);
this.queryCreator = new AotQueryCreator();
this.mappingContext = new MongoMappingContext();
}
@Override
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class));
}
@Override
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method,
RepositoryInformation repositoryInformation) {
if (AnnotatedElementUtils.hasAnnotation(method, Aggregation.class)) {
return null;
}
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
if (queryAnnotation != null) {
if (StringUtils.hasText(queryAnnotation.value())
&& Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) {
return null;
}
}
MongoQueryMethod queryMethod = new MongoQueryMethod(method, repositoryInformation, getProjectionFactory(),
mappingContext);
return MethodContributor.forQueryMethod(queryMethod).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
boolean count, delete, exists;
StringQuery query;
if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.value())) {
query = new StringQuery(queryAnnotation.value());
count = queryAnnotation.count();
delete = queryAnnotation.delete();
exists = queryAnnotation.exists();
} else {
PartTree partTree = new PartTree(context.getMethod().getName(),
context.getRepositoryInformation().getDomainType());
query = queryCreator.createQuery(partTree, context.getMethod().getParameterCount());
count = partTree.isCountProjection();
delete = partTree.isDelete();
exists = partTree.isExistsProjection();
}
if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) {
query.sort(queryAnnotation.sort());
}
if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) {
query.fields(queryAnnotation.fields());
}
writeStringQuery(context, builder, count, delete, exists, query, queryMethod);
return builder.build();
});
}
private static void writeStringQuery(AotQueryMethodGenerationContext context, CodeBlock.Builder body, boolean count,
boolean delete, boolean exists, StringQuery query, MongoQueryMethod queryMethod) {
body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
QueryBlockBuilder queryBlockBuilder = MongoBlocks.queryBlockBuilder(context, queryMethod).filter(query);
if (delete) {
String deleteQueryVariableName = "deleteQuery";
body.add(queryBlockBuilder.usingQueryVariableName(deleteQueryVariableName).build());
body.add(
MongoBlocks.deleteExecutionBlockBuilder(context, queryMethod).referencing(deleteQueryVariableName).build());
} else {
String filterQueryVariableName = "filterQuery";
body.add(queryBlockBuilder.usingQueryVariableName(filterQueryVariableName).build());
body.add(MongoBlocks.queryExecutionBlockBuilder(context, queryMethod).exists(exists).count(count)
.referencing(filterQueryVariableName).build());
}
}
}

46
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java

@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
*/
package org.springframework.data.mongodb.core.query;
import static org.springframework.util.ObjectUtils.*;
import static org.springframework.util.ObjectUtils.nullSafeHashCode;
import java.util.ArrayList;
import java.util.Arrays;
@ -29,21 +29,9 @@ import java.util.Map.Entry; @@ -29,21 +29,9 @@ import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.mongodb.MongoClientSettings;
import org.bson.BsonReader;
import org.bson.BsonRegularExpression;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.Document;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.DocumentCodec;
import org.bson.codecs.DocumentCodecProvider;
import org.bson.codecs.Encoder;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.types.Binary;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Example;
@ -342,8 +330,7 @@ public class Criteria implements CriteriaDefinition { @@ -342,8 +330,7 @@ public class Criteria implements CriteriaDefinition {
throw new InvalidMongoDbApiUsageException(
"You can only pass in one argument of type " + values[1].getClass().getName());
}
criteria.put("$in", Arrays.asList(values));
return this;
return this.in(Arrays.asList(values));
}
/**
@ -355,7 +342,13 @@ public class Criteria implements CriteriaDefinition { @@ -355,7 +342,13 @@ public class Criteria implements CriteriaDefinition {
*/
@Contract("_ -> this")
public Criteria in(Collection<?> values) {
criteria.put("$in", values);
ArrayList<?> objects = new ArrayList<>(values);
if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) {
criteria.put("$in", placeholder);
} else {
criteria.put("$in", objects);
}
return this;
}
@ -380,7 +373,13 @@ public class Criteria implements CriteriaDefinition { @@ -380,7 +373,13 @@ public class Criteria implements CriteriaDefinition {
*/
@Contract("_ -> this")
public Criteria nin(Collection<?> values) {
criteria.put("$nin", values);
ArrayList<?> objects = new ArrayList<>(values);
if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) {
criteria.put("$nin", placeholder);
} else {
criteria.put("$nin", objects);
}
return this;
}
@ -926,6 +925,19 @@ public class Criteria implements CriteriaDefinition { @@ -926,6 +925,19 @@ public class Criteria implements CriteriaDefinition {
return registerCriteriaChainElement(new Criteria("$and").is(bsonList));
}
/**
* Creates a criterion using the given {@literal operator}.
*
* @param operator the native MongoDB operator.
* @param value the operator value
* @return this
* @since 5.0
*/
public Criteria raw(String operator, Object value) {
criteria.put(operator, value);
return this;
}
private Criteria registerCriteriaChainElement(Criteria criteria) {
if (lastOperatorWasNot()) {

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

@ -40,16 +40,36 @@ public interface CriteriaDefinition { @@ -40,16 +40,36 @@ public interface CriteriaDefinition {
@Nullable
String getKey();
/**
* A placeholder expression used when rending queries to JSON.
*
* @since 5.0
* @author Christoph Strobl
*/
class Placeholder {
Object value;
private final Object expression;
/**
* Create a new placeholder for index bindable parameter.
*
* @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));
}
public static Placeholder placeholder(String expression) {
return new Placeholder(expression);
}
public Placeholder(Object value) {
this.value = value;
Placeholder(Object value) {
this.expression = value;
}
public Object getValue() {
return value;
return expression;
}
@Override

56
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* 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.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.data.repository.aot.generate.QueryMetadata;
/**
* An {@link MongoInteraction aggregation interaction}.
*
* @author Christoph Strobl
* @since 5.0
*/
class AggregationInteraction extends MongoInteraction implements QueryMetadata {
private final StringAggregation aggregation;
AggregationInteraction(String[] raw) {
this.aggregation = new StringAggregation(raw);
}
List<String> stages() {
return Arrays.asList(aggregation.pipeline());
}
@Override
InteractionType getExecutionType() {
return InteractionType.AGGREGATION;
}
@Override
public Map<String, Object> serialize() {
return Map.of(pipelineSerializationKey(), stages());
}
protected String pipelineSerializationKey() {
return "pipeline";
}
}

52
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
/*
* 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.Map;
/**
* An {@link MongoInteraction} to execute an aggregation update.
*
* @author Christoph Strobl
* @since 5.0
*/
class AggregationUpdateInteraction extends AggregationInteraction {
private final QueryInteraction filter;
AggregationUpdateInteraction(QueryInteraction filter, String[] raw) {
super(raw);
this.filter = filter;
}
QueryInteraction getFilter() {
return filter;
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> serialized = filter.serialize();
serialized.putAll(super.serialize());
return serialized;
}
@Override
protected String pipelineSerializationKey() {
return "update-" + super.pipelineSerializationKey();
}
}

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java

@ -15,11 +15,11 @@ @@ -15,11 +15,11 @@
*/
package org.springframework.data.mongodb.repository.aot;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.data.aot.AotContext;
import org.springframework.data.mongodb.aot.LazyLoadingProxyAotProcessor;
import org.springframework.data.mongodb.aot.MongoAotPredicates;
import org.springframework.data.mongodb.aot.generated.MongoRepositoryContributor;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor;
@ -34,7 +34,7 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr @@ -34,7 +34,7 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr
private final LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor();
@Override
protected RepositoryContributor contribute(AotRepositoryContext repositoryContext,
protected @Nullable RepositoryContributor contribute(AotRepositoryContext repositoryContext,
GenerationContext generationContext) {
// do some custom type registration here
super.contribute(repositoryContext, generationContext);

42
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java → spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.aot.generated;
package org.springframework.data.mongodb.repository.aot;
import java.util.Iterator;
import java.util.List;
@ -21,6 +21,8 @@ import java.util.stream.Collectors; @@ -21,6 +21,8 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.bson.conversions.Bson;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.ScrollPosition;
@ -41,15 +43,14 @@ import org.springframework.data.mongodb.repository.query.MongoParameterAccessor; @@ -41,15 +43,14 @@ import org.springframework.data.mongodb.repository.query.MongoParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoQueryCreator;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import com.mongodb.DBRef;
/**
* @author Christoph Strobl
* @since 2025/01
* @since 5.0
*/
public class AotQueryCreator {
class AotQueryCreator {
private MongoMappingContext mappingContext;
@ -64,13 +65,14 @@ public class AotQueryCreator { @@ -64,13 +65,14 @@ public class AotQueryCreator {
this.mappingContext = mongoMappingContext;
}
@SuppressWarnings("NullAway")
StringQuery createQuery(PartTree partTree, int parameterCount) {
Query query = new MongoQueryCreator(partTree,
new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext)
.createQuery();
if(partTree.isLimiting()) {
if (partTree.isLimiting()) {
query.limit(partTree.getMaxResults());
}
return new StringQuery(query);
@ -88,13 +90,13 @@ public class AotQueryCreator { @@ -88,13 +90,13 @@ public class AotQueryCreator {
}
}
@NullUnmarked
enum PlaceholderWriter implements MongoWriter<Object> {
INSTANCE;
@Nullable
@Override
public Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation<?> typeInformation) {
public @Nullable Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation<?> typeInformation) {
return obj instanceof Placeholder p ? p.getValue() : obj;
}
@ -109,6 +111,7 @@ public class AotQueryCreator { @@ -109,6 +111,7 @@ public class AotQueryCreator {
}
}
@NullUnmarked
static class PlaceholderParameterAccessor implements MongoParameterAccessor {
private final List<Placeholder> placeholders;
@ -117,8 +120,7 @@ public class AotQueryCreator { @@ -117,8 +120,7 @@ public class AotQueryCreator {
if (parameterCount == 0) {
placeholders = List.of();
} else {
placeholders = IntStream.range(0, parameterCount).mapToObj(it -> new Placeholder("?" + it))
.collect(Collectors.toList());
placeholders = IntStream.range(0, parameterCount).mapToObj(Placeholder::indexed).collect(Collectors.toList());
}
}
@ -127,21 +129,18 @@ public class AotQueryCreator { @@ -127,21 +129,18 @@ public class AotQueryCreator {
return null;
}
@Nullable
@Override
public Point getGeoNearLocation() {
public @Nullable Point getGeoNearLocation() {
return null;
}
@Nullable
@Override
public TextCriteria getFullText() {
public @Nullable TextCriteria getFullText() {
return null;
}
@Nullable
@Override
public Collation getCollation() {
public @Nullable Collation getCollation() {
return null;
}
@ -150,15 +149,13 @@ public class AotQueryCreator { @@ -150,15 +149,13 @@ public class AotQueryCreator {
return placeholders.toArray();
}
@Nullable
@Override
public UpdateDefinition getUpdate() {
public @Nullable UpdateDefinition getUpdate() {
return null;
}
@Nullable
@Override
public ScrollPosition getScrollPosition() {
public @Nullable ScrollPosition getScrollPosition() {
return null;
}
@ -172,15 +169,13 @@ public class AotQueryCreator { @@ -172,15 +169,13 @@ public class AotQueryCreator {
return null;
}
@Nullable
@Override
public Class<?> findDynamicProjection() {
public @Nullable Class<?> findDynamicProjection() {
return null;
}
@Nullable
@Override
public Object getBindableValue(int index) {
public @Nullable Object getBindableValue(int index) {
return placeholders.get(index).getValue();
}
@ -195,5 +190,4 @@ public class AotQueryCreator { @@ -195,5 +190,4 @@ public class AotQueryCreator {
return ((List) placeholders).iterator();
}
}
}

147
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
/*
* 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.Map;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mongodb.BindableMongoExpression;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.FieldName;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* @author Christoph Strobl
*/
public class MongoAotRepositoryFragmentSupport {
private final RepositoryMetadata repositoryMetadata;
private final MongoOperations mongoOperations;
private final MongoConverter mongoConverter;
private final ProjectionFactory projectionFactory;
protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations,
RepositoryFactoryBeanSupport.FragmentCreationContext context) {
this(mongoOperations, context.getRepositoryMetadata(), context.getProjectionFactory());
}
protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations, RepositoryMetadata repositoryMetadata,
ProjectionFactory projectionFactory) {
this.mongoOperations = mongoOperations;
this.mongoConverter = mongoOperations.getConverter();
this.repositoryMetadata = repositoryMetadata;
this.projectionFactory = projectionFactory;
}
protected Document bindParameters(String source, Object[] parameters) {
return new BindableMongoExpression(source, this.mongoConverter, parameters).toDocument();
}
protected BasicQuery createQuery(String queryString, Object[] parameters) {
Document queryDocument = bindParameters(queryString, parameters);
return new BasicQuery(queryDocument);
}
protected AggregationPipeline createPipeline(List<Object> rawStages) {
List<AggregationOperation> stages = new ArrayList<>(rawStages.size());
boolean first = true;
for (Object rawStage : rawStages) {
if (rawStage instanceof Document stageDocument) {
if (first) {
stages.add((ctx) -> ctx.getMappedObject(stageDocument));
} else {
stages.add((ctx) -> stageDocument);
}
} else if (rawStage instanceof AggregationOperation aggregationOperation) {
stages.add(aggregationOperation);
} else {
throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass()));
}
if (first) {
first = false;
}
}
return new AggregationPipeline(stages);
}
protected List<Object> convertSimpleRawResults(Class<?> targetType, List<Document> rawResults) {
List<Object> list = new ArrayList<>(rawResults.size());
for (Document it : rawResults) {
list.add(extractSimpleTypeResult(it, targetType, mongoConverter));
}
return list;
}
private static <T> @Nullable T extractSimpleTypeResult(@Nullable Document source, Class<T> targetType,
MongoConverter converter) {
if (ObjectUtils.isEmpty(source)) {
return null;
}
if (source.size() == 1) {
return getPotentiallyConvertedSimpleTypeValue(converter, source.values().iterator().next(), targetType);
}
Document intermediate = new Document(source);
intermediate.remove(FieldName.ID.name());
if (intermediate.size() == 1) {
return getPotentiallyConvertedSimpleTypeValue(converter, intermediate.values().iterator().next(), targetType);
}
for (Map.Entry<String, Object> entry : intermediate.entrySet()) {
if (entry != null && ClassUtils.isAssignable(targetType, entry.getValue().getClass())) {
return targetType.cast(entry.getValue());
}
}
throw new IllegalArgumentException(
String.format("o_O no entry of type %s found in %s.", targetType.getSimpleName(), source.toJson()));
}
@Nullable
@SuppressWarnings("unchecked")
private static <T> T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value,
Class<T> targetType) {
if (value == null) {
return null;
}
if (ClassUtils.isAssignableValue(targetType, value)) {
return (T) value;
}
return converter.getConversionService().convert(value, targetType);
}
}

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

@ -0,0 +1,777 @@ @@ -0,0 +1,777 @@
/*
* 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.Optional;
import java.util.regex.Pattern;
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.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.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.javapoet.ClassName;
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;
/**
* {@link CodeBlock} generator for common tasks.
*
* @author Christoph Strobl
* @since 5.0
*/
class MongoCodeBlocks {
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
/**
* Builder for generating query parsing {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return new instance of {@link QueryCodeBlockBuilder}.
*/
static QueryCodeBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new QueryCodeBlockBuilder(context, queryMethod);
}
/**
* Builder for generating finder query execution {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return
*/
static QueryExecutionCodeBlockBuilder queryExecutionBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new QueryExecutionCodeBlockBuilder(context, queryMethod);
}
/**
* Builder for generating delete execution {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return
*/
static DeleteExecutionCodeBlockBuilder deleteExecutionBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new DeleteExecutionCodeBlockBuilder(context, queryMethod);
}
/**
* Builder for generating update parsing {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return
*/
static UpdateCodeBlockBuilder updateBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new UpdateCodeBlockBuilder(context, queryMethod);
}
/**
* Builder for generating update execution {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return
*/
static UpdateExecutionCodeBlockBuilder updateExecutionBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new UpdateExecutionCodeBlockBuilder(context, queryMethod);
}
/**
* Builder for generating aggregation (pipeline) parsing {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return
*/
static AggregationCodeBlockBuilder aggregationBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
return new AggregationCodeBlockBuilder(context, queryMethod);
}
/**
* Builder for generating aggregation execution {@link CodeBlock}.
*
* @param context
* @param queryMethod
* @return
*/
static AggregationExecutionCodeBlockBuilder aggregationExecutionBlockBuilder(AotQueryMethodGenerationContext context,
MongoQueryMethod queryMethod) {
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();
boolean isProjecting = context.getActualReturnType() != null
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
context.getActualReturnType());
Object actualReturnType = isProjecting ? context.getActualReturnType().getType()
: context.getRepositoryInformation().getDomainType();
builder.add("\n");
builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class,
context.getRepositoryInformation().getDomainType(), mongoOpsRef,
context.getRepositoryInformation().getDomainType());
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())
? ClassName.get(context.getMethod().getReturnType())
: queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType;
builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecution.class,
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;
builder.addStatement("$T<$T> updater = $L.update($T.class)", ExecutableUpdate.class,
context.getRepositoryInformation().getDomainType(), mongoOpsRef,
context.getRepositoryInformation().getDomainType());
Class<?> returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType());
if (ReflectionUtils.isVoid(returnType)) {
builder.addStatement("updater.matching($L).apply($L).all()", queryVariableName, updateReference);
} else if (ClassUtils.isAssignable(Long.class, returnType)) {
builder.addStatement("return updater.matching($L).apply($L).all().getModifiedCount()", queryVariableName,
updateReference);
} else {
builder.addStatement("$T modifiedCount = updater.matching($L).apply($L).all().getModifiedCount()", Long.class,
queryVariableName, updateReference);
builder.addStatement("return $T.convertNumberToTargetClass(modifiedCount, $T.class)", NumberUtils.class,
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());
builder.addStatement("$T results = $L.aggregate($L, $T.class)", AggregationResults.class, mongoOpsRef,
aggregationVariableName, outputType);
if (!queryMethod.isCollectionQuery()) {
builder.addStatement(
"return $T.<$T>firstElement(convertSimpleRawResults($T.class, results.getMappedResults()))",
CollectionUtils.class, returnType, returnType);
} else {
builder.addStatement("return convertSimpleRawResults($T.class, results.getMappedResults())", returnType);
}
} else {
if (queryMethod.isSliceQuery()) {
builder.addStatement("$T results = $L.aggregate($L, $T.class)", AggregationResults.class, mongoOpsRef,
aggregationVariableName, outputType);
builder.addStatement("boolean hasNext = results.getMappedResults().size() > $L.getPageSize()",
context.getPageableParameterName());
builder.addStatement(
"return new $T<>(hasNext ? results.getMappedResults().subList(0, $L.getPageSize()) : results.getMappedResults(), $L, hasNext)",
SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName());
} else {
builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef,
aggregationVariableName, outputType);
}
}
return builder.build();
}
}
@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();
Object actualReturnType = isProjecting ? context.getActualReturnType().getType()
: context.getRepositoryInformation().getDomainType();
builder.add("\n");
if (isProjecting) {
builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType,
mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType);
} else {
builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef,
context.getRepositoryInformation().getDomainType());
}
String terminatingMethod;
if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
terminatingMethod = "all()";
} else if (query.isCount()) {
terminatingMethod = "count()";
} else if (query.isExists()) {
terminatingMethod = "exists()";
} else {
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()";
}
if (queryMethod.isPageQuery()) {
builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class,
context.getPageableParameterName(), query.name());
} else if (queryMethod.isSliceQuery()) {
builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class,
context.getPageableParameterName(), query.name());
} else {
builder.addStatement("return finder.matching($L).$L", query.name(), terminatingMethod);
}
return builder.build();
}
}
@NullUnmarked
static class AggregationCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private AggregationInteraction source;
private 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 = aggregationVariableName + (pipelineOnly ? "" : "Pipeline");
builder.add(pipeline(pipelineName));
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();
String stagesVariableName = "stages";
builder.add(aggregationStages(stagesVariableName, 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,
stagesVariableName);
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("$T aggregationOptions = $T.builder()\n", AggregationOptions.class,
AggregationOptions.class);
optionsBuilder.indent();
for (CodeBlock optionBlock : options) {
optionsBuilder.add(optionBlock);
optionsBuilder.add("\n");
}
optionsBuilder.add(".build();\n");
optionsBuilder.unindent();
builder.add(optionsBuilder.build());
builder.addStatement("$L = $L.withOptions(aggregationOptions)", aggregationVariableName,
aggregationVariableName);
}
return builder.build();
}
private static 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 = "stage_%s".formatted(stageCounter++);
builder.add(renderExpressionToDocument(stage, stageName, arguments));
builder.addStatement("stages.add($L)", stageName);
}
return builder.build();
}
private static CodeBlock sortingStage(String sortProvider) {
Builder builder = CodeBlock.builder();
builder.beginControlFlow("if($L.isSorted())", sortProvider);
builder.addStatement("$T sortDocument = new $T()", Document.class, Document.class);
builder.beginControlFlow("for ($T order : $L)", Order.class, sortProvider);
builder.addStatement("sortDocument.append(order.getProperty(), order.isAscending() ? 1 : -1);");
builder.endControlFlow();
builder.addStatement("stages.add(new $T($S, sortDocument))", Document.class, "$sort");
builder.endControlFlow();
return builder.build();
}
private static 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("stages.add($T.skip($L.getOffset()))", Aggregation.class, pageableProvider);
builder.endControlFlow();
if (slice) {
builder.addStatement("stages.add($T.limit($L.getPageSize() + 1))", Aggregation.class, pageableProvider);
} else {
builder.addStatement("stages.add($T.limit($L.getPageSize()))", Aggregation.class, pageableProvider);
}
builder.endControlFlow();
return builder.build();
}
private static CodeBlock limitingStage(String limitProvider) {
Builder builder = CodeBlock.builder();
builder.beginControlFlow("if($L.isLimited())", limitProvider);
builder.addStatement("stages.add($T.limit($L.max()))", Aggregation.class, limitProvider);
builder.endControlFlow();
return builder.build();
}
}
@NullUnmarked
static class QueryCodeBlockBuilder {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private QueryInteraction source;
private 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);
}
// TODO: Meta annotation
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)) {
String tmpVarName = "%sString".formatted(variableName);
builder.addStatement("String $L = $S", tmpVarName, source);
builder.addStatement("$T $L = new $T($T.parse($L))", BasicQuery.class, variableName, BasicQuery.class,
Document.class, tmpVarName);
} else {
String tmpVarName = "%sString".formatted(variableName);
builder.addStatement("String $L = $S", tmpVarName, source);
builder.addStatement("$T $L = createQuery($L, new $T[]{ $L })", BasicQuery.class, variableName, tmpVarName,
Object.class, StringUtils.collectionToDelimitedString(arguments, ", "));
}
return builder.build();
}
}
@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();
}
}
private static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName,
List<String> arguments) {
Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) {
builder.addStatement("$T $L = new $T()", Document.class, variableName, Document.class);
} else if (!containsPlaceholder(source)) {
String tmpVarName = "%sString".formatted(variableName);
builder.addStatement("String $L = $S", tmpVarName, source);
builder.addStatement("$T $L = $T.parse($L)", Document.class, variableName, Document.class, tmpVarName);
} else {
String tmpVarName = "%sString".formatted(variableName);
builder.addStatement("String $L = $S", tmpVarName, source);
builder.addStatement("$T $L = bindParameters($L, new $T[]{ $L })", Document.class, variableName, tmpVarName,
Object.class, StringUtils.collectionToDelimitedString(arguments, ", "));
}
return builder.build();
}
private static boolean containsPlaceholder(String source) {
return PARAMETER_BINDING_PATTERN.matcher(source).find();
}
}

62
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
/*
* 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;
/**
* Base abstraction for interactions with MongoDB.
*
* @author Christoph Strobl
* @since 5.0
*/
abstract class MongoInteraction {
abstract InteractionType getExecutionType();
boolean isAggregation() {
return InteractionType.AGGREGATION.equals(getExecutionType());
}
boolean isCount() {
return InteractionType.COUNT.equals(getExecutionType());
}
boolean isDelete() {
return InteractionType.DELETE.equals(getExecutionType());
}
boolean isExists() {
return InteractionType.EXISTS.equals(getExecutionType());
}
boolean isUpdate() {
return InteractionType.UPDATE.equals(getExecutionType());
}
String name() {
if (isDelete()) {
return "deleteQuery";
}
if (isCount()) {
return "countQuery";
}
return "filterQuery";
}
enum InteractionType {
QUERY, COUNT, DELETE, EXISTS, UPDATE, AGGREGATION
}
}

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

@ -0,0 +1,286 @@ @@ -0,0 +1,286 @@
/*
* 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.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.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 java.lang.reflect.Method;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.Update;
import org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.QueryCodeBlockBuilder;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata;
import org.springframework.data.repository.aot.generate.MethodContributor;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec.Builder;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* MongoDB specific {@link RepositoryContributor}.
*
* @author Christoph Strobl
* @since 5.0
*/
public class MongoRepositoryContributor extends RepositoryContributor {
private static final Log logger = LogFactory.getLog(RepositoryContributor.class);
private final AotQueryCreator queryCreator;
private final MongoMappingContext mappingContext;
public MongoRepositoryContributor(AotRepositoryContext repositoryContext) {
super(repositoryContext);
this.queryCreator = new AotQueryCreator();
this.mappingContext = new MongoMappingContext();
}
@Override
protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata,
Builder builder) {
builder.superclass(TypeName.get(MongoAotRepositoryFragmentSupport.class));
}
@Override
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class));
constructorBuilder.addParameter("context", TypeName.get(RepositoryFactoryBeanSupport.FragmentCreationContext.class),
false);
constructorBuilder.customize((repositoryInformation, builder) -> {
builder.addStatement("super(operations, context)");
});
}
@Override
@SuppressWarnings("NullAway")
protected @Nullable MethodContributor<? extends QueryMethod> contributeQueryMethod(Method method,
RepositoryInformation repositoryInformation) {
MongoQueryMethod queryMethod = new MongoQueryMethod(method, repositoryInformation, getProjectionFactory(),
mappingContext);
if (backoff(queryMethod)) {
return null;
}
try {
if (queryMethod.hasAnnotatedAggregation()) {
AggregationInteraction aggregation = new AggregationInteraction(queryMethod.getAnnotatedAggregation());
return aggregationMethodContributor(queryMethod, aggregation);
}
QueryInteraction query = createStringQuery(repositoryInformation, queryMethod,
AnnotatedElementUtils.findMergedAnnotation(method, Query.class), method.getParameterCount());
if (queryMethod.hasAnnotatedQuery()) {
if (StringUtils.hasText(queryMethod.getAnnotatedQuery())
&& Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryMethod.getAnnotatedQuery()).find()) {
if (logger.isDebugEnabled()) {
logger.debug(
"Skipping AOT generation for [%s]. SpEL expressions are not supported".formatted(method.getName()));
}
return MethodContributor.forQueryMethod(queryMethod).metadataOnly(query);
}
}
if (query.isDelete()) {
return deleteMethodContributor(queryMethod, query);
}
if (queryMethod.isModifyingQuery()) {
Update updateSource = queryMethod.getUpdateSource();
if (StringUtils.hasText(updateSource.value())) {
UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value()));
return updateMethodContributor(queryMethod, update);
}
if (!ObjectUtils.isEmpty(updateSource.pipeline())) {
AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline());
return aggregationUpdateMethodContributor(queryMethod, update);
}
}
return queryMethodContributor(queryMethod, query);
} catch (RuntimeException codeGenerationError) {
if (logger.isErrorEnabled()) {
logger.error("Failed to generate code for [%s] [%s]".formatted(repositoryInformation.getRepositoryInterface(),
method.getName()), codeGenerationError);
}
}
return null;
}
@SuppressWarnings("NullAway")
private QueryInteraction createStringQuery(RepositoryInformation repositoryInformation, MongoQueryMethod queryMethod,
@Nullable Query queryAnnotation, int parameterCount) {
QueryInteraction query;
if (queryMethod.hasAnnotatedQuery() && queryAnnotation != null) {
query = new QueryInteraction(new StringQuery(queryMethod.getAnnotatedQuery()), queryAnnotation.count(),
queryAnnotation.delete(), queryAnnotation.exists());
} else {
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
query = new QueryInteraction(queryCreator.createQuery(partTree, parameterCount), partTree.isCountProjection(),
partTree.isDelete(), partTree.isExistsProjection());
}
if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) {
query = query.withSort(queryAnnotation.sort());
}
if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) {
query = query.withFields(queryAnnotation.fields());
}
return query;
}
private static boolean backoff(MongoQueryMethod method) {
boolean skip = method.isGeoNearQuery() || method.isScrollQuery() || method.isStreamQuery();
if (skip && logger.isDebugEnabled()) {
logger.debug("Skipping AOT generation for [%s]. Method is either geo-near, streaming or scrolling query"
.formatted(method.getName()));
}
return skip;
}
private static MethodContributor<MongoQueryMethod> aggregationMethodContributor(MongoQueryMethod queryMethod,
AggregationInteraction aggregation) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(aggregation).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
builder.add(aggregationBlockBuilder(context, queryMethod).stages(aggregation)
.usingAggregationVariableName("aggregation").build());
builder.add(aggregationExecutionBlockBuilder(context, queryMethod).referencing("aggregation").build());
return builder.build();
});
}
private static MethodContributor<MongoQueryMethod> updateMethodContributor(MongoQueryMethod queryMethod,
UpdateInteraction update) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
// update filter
String filterVariableName = update.name();
builder.add(queryBlockBuilder(context, queryMethod).filter(update.getFilter())
.usingQueryVariableName(filterVariableName).build());
// update definition
String updateVariableName = "updateDefinition";
builder.add(
updateBlockBuilder(context, queryMethod).update(update).usingUpdateVariableName(updateVariableName).build());
builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName)
.referencingUpdate(updateVariableName).build());
return builder.build();
});
}
private static MethodContributor<MongoQueryMethod> aggregationUpdateMethodContributor(MongoQueryMethod queryMethod,
AggregationUpdateInteraction update) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
// update filter
String filterVariableName = update.name();
QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(update.getFilter());
builder.add(queryCodeBlockBuilder.usingQueryVariableName(filterVariableName).build());
// update definition
String updateVariableName = "updateDefinition";
builder.add(aggregationBlockBuilder(context, queryMethod).stages(update)
.usingAggregationVariableName(updateVariableName).pipelineOnly(true).build());
builder.addStatement("$T aggregationUpdate = $T.from($L.getOperations())", AggregationUpdate.class,
AggregationUpdate.class, updateVariableName);
builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName)
.referencingUpdate("aggregationUpdate").build());
return builder.build();
});
}
private static MethodContributor<MongoQueryMethod> deleteMethodContributor(MongoQueryMethod queryMethod,
QueryInteraction query) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query);
builder.add(queryCodeBlockBuilder.usingQueryVariableName(query.name()).build());
builder.add(deleteExecutionBlockBuilder(context, queryMethod).referencing(query.name()).build());
return builder.build();
});
}
private static MethodContributor<MongoQueryMethod> queryMethodContributor(MongoQueryMethod queryMethod,
QueryInteraction query) {
return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> {
CodeBlock.Builder builder = CodeBlock.builder();
builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query);
builder.add(queryCodeBlockBuilder.usingQueryVariableName(query.name()).build());
builder.add(queryExecutionBlockBuilder(context, queryMethod).forQuery(query).build());
return builder.build();
});
}
}

83
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
/*
* 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.repository.aot.generate.QueryMetadata;
import org.springframework.util.StringUtils;
/**
* An {@link MongoInteraction} to execute a query.
*
* @author Christoph Strobl
* @since 5.0
*/
class QueryInteraction extends MongoInteraction implements QueryMetadata {
private final StringQuery query;
private final InteractionType interactionType;
QueryInteraction(StringQuery query, boolean count, boolean delete, boolean exists) {
this.query = query;
if (count) {
interactionType = InteractionType.COUNT;
} else if (exists) {
interactionType = InteractionType.EXISTS;
} else if (delete) {
interactionType = InteractionType.DELETE;
} else {
interactionType = InteractionType.QUERY;
}
}
StringQuery getQuery() {
return query;
}
QueryInteraction withSort(String sort) {
query.sort(sort);
return this;
}
QueryInteraction withFields(String fields) {
query.fields(fields);
return this;
}
@Override
InteractionType getExecutionType() {
return interactionType;
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> serialized = new LinkedHashMap<>();
serialized.put("filter", query.getQueryString());
if (query.isSorted()) {
serialized.put("sort", query.getSortString());
}
if (StringUtils.hasText(query.getFieldsString())) {
serialized.put("fields", query.getFieldsString());
}
return serialized;
}
}

26
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.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 org.springframework.data.mongodb.repository.aot;
/**
* Value object holding the raw representation of an Aggregation Pipeline.
*
* @author Christoph Strobl
* @since 5.0
*/
record StringAggregation(String[] pipeline) {
}

51
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java → spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java

@ -1,19 +1,3 @@ @@ -1,19 +1,3 @@
/*
* 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.
*
@ -21,7 +5,7 @@ @@ -21,7 +5,7 @@
* 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
* 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,
@ -29,27 +13,29 @@ @@ -29,27 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.aot.generated;
package org.springframework.data.mongodb.repository.aot;
import java.util.Optional;
import java.util.Set;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Field;
import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
/**
* Helper to capture setting for AOT queries.
*
* @author Christoph Strobl
* @since 2025/01
* @since 5.0
*/
class StringQuery extends Query {
@ -58,8 +44,6 @@ class StringQuery extends Query { @@ -58,8 +44,6 @@ class StringQuery extends Query {
private @Nullable String sort;
private @Nullable String fields;
private ExecutionType executionType = ExecutionType.QUERY;
public StringQuery(Query query) {
this.delegate = query;
}
@ -69,11 +53,6 @@ class StringQuery extends Query { @@ -69,11 +53,6 @@ class StringQuery extends Query {
this.raw = query;
}
public StringQuery forCount() {
this.executionType = ExecutionType.COUNT;
return this;
}
@Nullable
String getQueryString() {
@ -104,7 +83,7 @@ class StringQuery extends Query { @@ -104,7 +83,7 @@ class StringQuery extends Query {
}
@Override
public ReadConcern getReadConcern() {
public @Nullable ReadConcern getReadConcern() {
return delegate.getReadConcern();
}
@ -114,7 +93,7 @@ class StringQuery extends Query { @@ -114,7 +93,7 @@ class StringQuery extends Query {
}
@Override
public ReadPreference getReadPreference() {
public @Nullable ReadPreference getReadPreference() {
return delegate.getReadPreference();
}
@ -124,8 +103,7 @@ class StringQuery extends Query { @@ -124,8 +103,7 @@ class StringQuery extends Query {
}
@Override
@Nullable
public KeysetScrollPosition getKeyset() {
public @Nullable KeysetScrollPosition getKeyset() {
return delegate.getKeyset();
}
@ -170,8 +148,7 @@ class StringQuery extends Query { @@ -170,8 +148,7 @@ class StringQuery extends Query {
}
@Override
@Nullable
public String getHint() {
public @Nullable String getHint() {
return delegate.getHint();
}
@ -216,12 +193,6 @@ class StringQuery extends Query { @@ -216,12 +193,6 @@ class StringQuery extends Query {
}
String toJson(Document source) {
StringBuffer buffer = new StringBuffer();
BsonUtils.writeJson(source).to(buffer);
return buffer.toString();
}
enum ExecutionType {
QUERY, COUNT, DELETE
return BsonUtils.writeJson(source).toJsonString();
}
}

27
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
/*
* 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;
/**
* @author Christoph Strobl
* @since 5.0
*/
record StringUpdate(String raw) {
String getUpdateString() {
return raw;
}
}

58
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
/*
* 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.Map;
import org.springframework.data.repository.aot.generate.QueryMetadata;
/**
* An {@link MongoInteraction} to execute an update.
*
* @author Christoph Strobl
* @since 5.0
*/
class UpdateInteraction extends MongoInteraction implements QueryMetadata {
private final QueryInteraction filter;
private final StringUpdate update;
UpdateInteraction(QueryInteraction filter, StringUpdate update) {
this.filter = filter;
this.update = update;
}
QueryInteraction getFilter() {
return filter;
}
StringUpdate getUpdate() {
return update;
}
@Override
public Map<String, Object> serialize() {
Map<String, Object> serialized = filter.serialize();
serialized.put("update", update.getUpdateString());
return serialized;
}
@Override
InteractionType getExecutionType() {
return InteractionType.UPDATE;
}
}

5
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java

@ -27,6 +27,7 @@ import org.springframework.data.mapping.model.ValueExpressionEvaluator; @@ -27,6 +27,7 @@ import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
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.AggregationOperation;
@ -69,6 +70,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { @@ -69,6 +70,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
private final MongoOperations operations;
private final ExecutableFind<?> executableFind;
private final ExecutableUpdate<?> executableUpdate;
private final ExecutableRemove<?> executableRemove;
private final Lazy<ParameterBindingDocumentCodec> codec = Lazy
.of(() -> new ParameterBindingDocumentCodec(getCodecRegistry()));
private final ValueExpressionDelegate valueExpressionDelegate;
@ -95,6 +97,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { @@ -95,6 +97,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
this.executableFind = operations.query(type);
this.executableUpdate = operations.update(type);
this.executableRemove = operations.remove(type);
this.valueExpressionDelegate = delegate;
this.valueEvaluationContextProvider = delegate.createValueContextProvider(method.getParameters());
}
@ -164,7 +167,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { @@ -164,7 +167,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {
if (isDeleteQuery()) {
return new DeleteExecution(operations, method);
return new DeleteExecution<>(executableRemove, method);
}
if (method.isModifyingQuery()) {

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

@ -184,9 +184,17 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -184,9 +184,17 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
case IS_NULL:
return criteria.is(null);
case NOT_IN:
return criteria.nin(nextAsList(parameters, part));
Object ninValue = parameters.next();
if(ninValue instanceof Placeholder) {
return criteria.raw("$nin", ninValue);
}
return criteria.nin(valueAsList(ninValue, part));
case IN:
return criteria.in(nextAsList(parameters, part));
Object inValue = parameters.next();
if(inValue instanceof Placeholder) {
return criteria.raw("$in", inValue);
}
return criteria.in(valueAsList(inValue, part));
case LIKE:
case STARTING_WITH:
case ENDING_WITH:
@ -201,7 +209,12 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -201,7 +209,12 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
Object param = parameters.next();
return param instanceof Pattern pattern ? criteria.regex(pattern) : criteria.regex(param.toString());
case EXISTS:
return criteria.exists((Boolean) parameters.next());
Object next = parameters.next();
if(next instanceof Placeholder placeholder) {
return criteria.raw("$exists", placeholder);
} else {
return criteria.exists((Boolean) next);
}
case TRUE:
return criteria.is(true);
case FALSE:
@ -320,7 +333,11 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -320,7 +333,11 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
Iterator<Object> parameters) {
if (property.isCollectionLike()) {
return criteria.in(nextAsList(parameters, part));
Object next = parameters.next();
if(next instanceof Placeholder) {
return criteria.raw("$in", next);
}
return criteria.in(valueAsList(next, part));
}
return addAppropriateLikeRegexTo(criteria, part, parameters.next());
@ -384,19 +401,19 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -384,19 +401,19 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
String.format("Expected parameter type of %s but got %s", type, parameter.getClass()));
}
private java.util.List<?> nextAsList(Iterator<Object> iterator, Part part) {
private java.util.List<?> valueAsList(Object value, Part part) {
Streamable<?> streamable = asStreamable(iterator.next());
Streamable<?> streamable = asStreamable(value);
if (!isSimpleComparisonPossible(part)) {
MatchMode matchMode = toMatchMode(part.getType());
String regexOptions = toRegexOptions(part);
streamable = streamable.map(it -> {
if (it instanceof String value) {
if (it instanceof String sv) {
return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(value, matchMode),
regexOptions);
return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode),
regexOptions);
}
return it;
});

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

@ -37,11 +37,11 @@ import org.springframework.data.mongodb.core.ExecutableRemoveOperation; @@ -37,11 +37,11 @@ import org.springframework.data.mongodb.core.ExecutableRemoveOperation;
import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove;
import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.repository.util.SliceUtils;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
@ -251,19 +251,39 @@ public interface MongoQueryExecution { @@ -251,19 +251,39 @@ public interface MongoQueryExecution {
}
}
final class DeleteExecutionX<T> implements MongoQueryExecution {
/**
* {@link MongoQueryExecution} removing documents matching the query.
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Artyom Gabeev
* @author Christoph Strobl
* @since 1.5
*/
final class DeleteExecution<T> implements MongoQueryExecution {
ExecutableRemoveOperation.ExecutableRemove<T> remove;
Type type;
private ExecutableRemoveOperation.ExecutableRemove<T> remove;
private Type type;
public DeleteExecutionX(ExecutableRemove<T> remove, Type type) {
public DeleteExecution(ExecutableRemove<T> remove, QueryMethod queryMethod) {
this.remove = remove;
if (queryMethod.isCollectionQuery()) {
this.type = Type.FIND_AND_REMOVE_ALL;
} else if (queryMethod.isQueryForEntity()
&& !ClassUtils.isPrimitiveOrWrapper(queryMethod.getReturnedObjectType())) {
this.type = Type.FIND_AND_REMOVE_ONE;
} else {
this.type = Type.ALL;
}
}
public DeleteExecution(ExecutableRemove<T> remove, Type type) {
this.remove = remove;
this.type = type;
}
@Nullable
@Override
public Object execute(Query query) {
public @Nullable Object execute(Query query) {
TerminatingRemove<T> doRemove = remove.matching(query);
if (Type.ALL.equals(type)) {
@ -274,7 +294,6 @@ public interface MongoQueryExecution { @@ -274,7 +294,6 @@ public interface MongoQueryExecution {
} else if (Type.FIND_AND_REMOVE_ONE.equals(type)) {
Iterator<T> removed = doRemove.findAndRemove().iterator();
return removed.hasNext() ? removed.next() : null;
}
throw new RuntimeException();
}
@ -284,48 +303,6 @@ public interface MongoQueryExecution { @@ -284,48 +303,6 @@ public interface MongoQueryExecution {
}
}
/**
* {@link MongoQueryExecution} removing documents matching the query.
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Artyom Gabeev
* @author Christoph Strobl
* @since 1.5
*/
final class DeleteExecution implements MongoQueryExecution {
private final MongoOperations operations;
private final MongoQueryMethod method;
public DeleteExecution(MongoOperations operations, MongoQueryMethod method) {
Assert.notNull(operations, "Operations must not be null");
Assert.notNull(method, "Method must not be null");
this.operations = operations;
this.method = method;
}
@Override
public @Nullable Object execute(Query query) {
String collectionName = method.getEntityInformation().getCollectionName();
Class<?> type = method.getEntityInformation().getJavaType();
if (method.isCollectionQuery()) {
return operations.findAllAndRemove(query, type, collectionName);
}
if (method.isQueryForEntity() && !ClassUtils.isPrimitiveOrWrapper(method.getReturnedObjectType())) {
return operations.findAndRemove(query, type, collectionName);
}
DeleteResult writeResult = operations.remove(query, type, collectionName);
return writeResult.wasAcknowledged() ? writeResult.getDeletedCount() : 0L;
}
}
/**
* {@link MongoQueryExecution} updating documents matching the query.
*

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java

@ -117,7 +117,7 @@ public class MongoQueryMethod extends QueryMethod { @@ -117,7 +117,7 @@ public class MongoQueryMethod extends QueryMethod {
* @return
*/
@Nullable
String getAnnotatedQuery() {
public String getAnnotatedQuery() {
return findAnnotatedQuery().orElse(null);
}
@ -204,7 +204,7 @@ public class MongoQueryMethod extends QueryMethod { @@ -204,7 +204,7 @@ public class MongoQueryMethod extends QueryMethod {
return doFindAnnotation(Query.class);
}
TypeInformation<?> getReturnType() {
public TypeInformation<?> getReturnType() {
return TypeInformation.fromReturnTypeOf(method);
}

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

@ -67,13 +67,13 @@ import org.bson.json.JsonParseException; @@ -67,13 +67,13 @@ import org.bson.json.JsonParseException;
import org.bson.types.Binary;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
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.util.json.SpringJsonWriter;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -337,7 +337,7 @@ public class BsonUtils { @@ -337,7 +337,7 @@ public class BsonUtils {
case BINARY -> {
BsonBinary binary = value.asBinary();
if(binary.getType() != BsonBinarySubType.VECTOR.getValue()) {
if (binary.getType() != BsonBinarySubType.VECTOR.getValue()) {
yield binary.getData();
}
yield value.asBinary().asVector();
@ -783,15 +783,39 @@ public class BsonUtils { @@ -783,15 +783,39 @@ public class BsonUtils {
return new Document(target);
}
/**
* Obtain a preconfigured {@link JsonWriter} allowing to render the given {@link Document} using a
* {@link CodecRegistry} containing a {@link PlaceholderCodec}.
*
* @param document the source document. Must not be {@literal null}.
* @return new instance of {@link JsonWriter}.
* @since 5.0
*/
public static JsonWriter writeJson(Document document) {
return sink -> {
SpringJsonWriter writer = new SpringJsonWriter(sink);
JSON_CODEC_REGISTRY.get(Document.class).encode(writer, document, EncoderContext.builder().build());
};
return sink -> JSON_CODEC_REGISTRY.get(Document.class).encode(new SpringJsonWriter(sink), document,
EncoderContext.builder().build());
}
/**
* Interface to pipe json rendering to a given sink.
*
* @since 5.0
*/
public interface JsonWriter {
/**
* Write the json output to the given sink.
*
* @param sink the output target
*/
void to(StringBuffer sink);
default String toJsonString() {
StringBuffer buffer = new StringBuffer();
to(buffer);
return buffer.toString();
}
}
@Contract("null -> null")
@ -1007,6 +1031,14 @@ public class BsonUtils { @@ -1007,6 +1031,14 @@ public class BsonUtils {
}
}
/**
* Internal {@link Codec} implementation to write
* {@link org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder placeholders}.
*
* @since 5.0
* @author Christoph Strobl
*/
@NullUnmarked
static class PlaceholderCodec implements Codec<Placeholder> {
@Override

15
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java → spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
package org.springframework.data.mongodb.util;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
@ -30,13 +30,18 @@ import org.bson.BsonTimestamp; @@ -30,13 +30,18 @@ import org.bson.BsonTimestamp;
import org.bson.BsonWriter;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.jspecify.annotations.NullUnmarked;
import org.springframework.util.StringUtils;
/**
* Internal {@link BsonWriter} implementation that allows to render {@link #writePlaceholder(String) placeholders} as
* {@code ?0}.
*
* @author Christoph Strobl
* @since 2025/01
* @since 5.0
*/
public class SpringJsonWriter implements BsonWriter {
@NullUnmarked
class SpringJsonWriter implements BsonWriter {
private final StringBuffer buffer;
@ -49,6 +54,7 @@ public class SpringJsonWriter implements BsonWriter { @@ -49,6 +54,7 @@ public class SpringJsonWriter implements BsonWriter {
}
private static class JsonContext {
private final JsonContext parentContext;
private final JsonContextType contextType;
private boolean hasElements;
@ -450,6 +456,9 @@ public class SpringJsonWriter implements BsonWriter { @@ -450,6 +456,9 @@ public class SpringJsonWriter implements BsonWriter {
}
/**
* @param placeholder
*/
public void writePlaceholder(String placeholder) {
write(placeholder);
}

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

@ -1,19 +1,3 @@ @@ -1,19 +1,3 @@
/*
* 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.
*
@ -21,7 +5,7 @@ @@ -21,7 +5,7 @@
* 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
* 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,
@ -37,7 +21,6 @@ import org.springframework.data.mongodb.core.mapping.Field; @@ -37,7 +21,6 @@ import org.springframework.data.mongodb.core.mapping.Field;
/**
* @author Christoph Strobl
* @since 2025/01
*/
public class User {
@ -51,6 +34,7 @@ public class User { @@ -51,6 +34,7 @@ public class User {
Instant registrationDate;
Instant lastSeen;
Long visits;
public String getId() {
return id;
@ -99,4 +83,12 @@ public class User { @@ -99,4 +83,12 @@ public class User {
public void setLastSeen(Instant lastSeen) {
this.lastSeen = lastSeen;
}
public Long getVisits() {
return visits;
}
public void setVisits(Long visits) {
this.visits = visits;
}
}

1
spring-data-mongodb/src/test/java/example/aot/UserProjection.java

@ -19,7 +19,6 @@ import java.time.Instant; @@ -19,7 +19,6 @@ import java.time.Instant;
/**
* @author Christoph Strobl
* @since 2025/01
*/
public interface UserProjection {

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

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
/*
* Copyright 2025. the original author or authors.
* 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
* 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,
@ -15,21 +15,30 @@ @@ -15,21 +15,30 @@
*/
package example.aot;
import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
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.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.Hint;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.Update;
import org.springframework.data.repository.CrudRepository;
/**
* @author Christoph Strobl
* @since 2025/01
*/
public interface UserRepository extends CrudRepository<User, String> {
@ -47,6 +56,28 @@ public interface UserRepository extends CrudRepository<User, String> { @@ -47,6 +56,28 @@ public interface UserRepository extends CrudRepository<User, String> {
List<User> findByLastnameStartingWith(String lastname);
List<User> findByLastnameEndsWith(String postfix);
List<User> findByFirstnameLike(String firstname);
List<User> findByFirstnameNotLike(String firstname);
List<User> findByUsernameIn(Collection<String> usernames);
List<User> findByUsernameNotIn(Collection<String> usernames);
List<User> findByFirstnameAndLastname(String firstname, String lastname);
List<User> findByFirstnameOrLastname(String firstname, String lastname);
List<User> findByVisitsBetween(long from, long to);
List<User> findByLastSeenGreaterThan(Instant time);
List<User> findByVisitsExists(boolean exists);
List<User> findByLastnameNot(String lastname);
List<User> findTop2ByLastnameStartingWith(String lastname);
List<User> findByLastnameStartingWithOrderByUsername(String lastname);
@ -66,6 +97,7 @@ public interface UserRepository extends CrudRepository<User, String> { @@ -66,6 +97,7 @@ public interface UserRepository extends CrudRepository<User, String> {
// TODO: Streaming
// TODO: Scrolling
// TODO: GeoQueries
// TODO: TextSearch
/* Annotated Queries */
@ -121,8 +153,17 @@ public interface UserRepository extends CrudRepository<User, String> { @@ -121,8 +153,17 @@ public interface UserRepository extends CrudRepository<User, String> {
@Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true)
List<User> deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname);
// TODO: updates
// TODO: Aggregations
/* Updates */
@Update("{ '$inc' : { 'visits' : ?1 } }")
int findUserAndIncrementVisitsByLastname(String lastname, int increment);
@Query("{ 'lastname' : ?0 }")
@Update("{ '$inc' : { 'visits' : ?1 } }")
int updateAllByLastname(String lastname, int increment);
@Update(pipeline = { "{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }" })
void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment);
/* Derived With Annotated Options */
@ -143,4 +184,95 @@ public interface UserRepository extends CrudRepository<User, String> { @@ -143,4 +184,95 @@ public interface UserRepository extends CrudRepository<User, String> {
Page<UserProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
/* Aggregations */
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$project': { '_id' : '$last_name' } }" })
List<String> findAllLastnames();
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
List<UserAggregate> groupByLastnameAnd(String property);
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
List<UserAggregate> groupByLastnameAnd(String property, Pageable pageable);
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
Slice<UserAggregate> groupByLastnameAndReturnPage(String property, Pageable pageable);
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" })
AggregationResults<UserAggregate> groupByLastnameAndAsAggregationResults(String property);
@Aggregation(pipeline = { //
"{ '$match' : { 'posts' : { '$ne' : null } } }", //
"{ '$project': { 'nrPosts' : {'$size': '$posts' } } }", //
"{ '$group' : { '_id' : null, 'total' : { $sum: '$nrPosts' } } }" })
int sumPosts();
@Hint("ln-idx")
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$project': { '_id' : '$last_name' } }" })
List<String> findAllLastnamesUsingIndex();
@ReadPreference("no-such-read-preference")
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$project': { '_id' : '$last_name' } }" })
List<String> findAllLastnamesWithReadPreference();
@Aggregation(pipeline = { //
"{ '$match' : { 'last_name' : { '$ne' : null } } }", //
"{ '$project': { '_id' : '$last_name' } }" }, collation = "no_collation")
List<String> findAllLastnamesWithCollation();
class UserAggregate {
@Id //
private final String lastname;
private final Set<String> names;
public UserAggregate(String lastname, Collection<String> names) {
this.lastname = lastname;
this.names = new HashSet<>(names);
}
public String getLastname() {
return this.lastname;
}
public Set<String> getNames() {
return this.names;
}
@Override
public String toString() {
return "UserAggregate{" + "lastname='" + lastname + '\'' + ", names=" + names + '}';
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserAggregate that = (UserAggregate) o;
return Objects.equals(lastname, that.lastname) && names.equals(that.names);
}
@Override
public int hashCode() {
return Objects.hash(lastname, names);
}
}
}

18
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java

@ -1,19 +1,3 @@ @@ -1,19 +1,3 @@
/*
* 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.
*
@ -21,7 +5,7 @@ @@ -21,7 +5,7 @@
* 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
* 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,

662
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java

@ -1,662 +0,0 @@ @@ -1,662 +0,0 @@
/*
* 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.aot.generated;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import example.aot.User;
import example.aot.UserProjection;
import example.aot.UserRepository;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.bson.Document;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
import org.springframework.data.mongodb.test.util.MongoTestUtils;
import org.springframework.data.util.Lazy;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.StringUtils;
import com.mongodb.client.MongoClient;
/**
* @author Christoph Strobl
* @since 2025/01
*/
@ExtendWith(MongoClientExtension.class)
public class MongoRepositoryContributorTests {
private static final String DB_NAME = "aot-repo-tests";
private static Verifyer generated;
@Client static MongoClient client;
@BeforeAll
static void beforeAll() {
TestMongoAotRepositoryContext aotContext = new TestMongoAotRepositoryContext(UserRepository.class, null);
TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class);
new MongoRepositoryContributor(aotContext).contribute(generationContext);
AbstractBeanDefinition mongoTemplate = BeanDefinitionBuilder.rootBeanDefinition(MongoTestTemplate.class)
.addConstructorArgValue(DB_NAME).getBeanDefinition();
AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
.genericBeanDefinition("example.aot.UserRepositoryImpl__Aot").addConstructorArgReference("mongoOperations")
.getBeanDefinition();
generated = generateContext(generationContext) //
.register("mongoOperations", mongoTemplate) //
.register("aotUserRepository", aotGeneratedRepository);
}
@BeforeEach
void beforeEach() {
MongoTestUtils.flushCollection(DB_NAME, "user", client);
initUsers();
}
@Test
void testFindDerivedFinderSingleEntity() {
generated.verify(methodInvoker -> {
User user = methodInvoker.invoke("findOneByUsername", "yoda").onBean("aotUserRepository");
assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
});
}
@Test
void testFindDerivedFinderOptionalEntity() {
generated.verify(methodInvoker -> {
Optional<User> user = methodInvoker.invoke("findOptionalOneByUsername", "yoda").onBean("aotUserRepository");
assertThat(user).isNotNull().containsInstanceOf(User.class)
.hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda"));
});
}
@Test
void testDerivedCount() {
generated.verify(methodInvoker -> {
Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository");
assertThat(value).isEqualTo(2L);
});
}
@Test
void testDerivedExists() {
generated.verify(methodInvoker -> {
Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository");
assertThat(exists).isTrue();
});
}
@Test
void testDerivedFinderWithoutArguments() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository");
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
});
}
@Test
void testCountWorksAsExpected() {
generated.verify(methodInvoker -> {
Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository");
assertThat(value).isEqualTo(2L);
});
}
@Test
void testDerivedFinderReturningList() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han");
});
}
@Test
void testLimitedDerivedFinder() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testSortedDerivedFinder() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWithOrderByUsername", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
});
}
@Test
void testDerivedFinderWithLimitArgument() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testDerivedFinderWithSort() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username"))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
});
}
@Test
void testDerivedFinderWithSortAndLimit() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username"), Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@Test
void testDerivedFinderReturningListWithPageable() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker
.invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username")))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@Test
void testDerivedFinderReturningPage() {
generated.verify(methodInvoker -> {
Page<User> page = methodInvoker
.invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username")))
.onBean("aotUserRepository");
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@Test
void testDerivedFinderReturningSlice() {
generated.verify(methodInvoker -> {
Slice<User> slice = methodInvoker
.invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username")))
.onBean("aotUserRepository");
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@Test
void testAnnotatedFinderReturningSingleValueWithQuery() {
generated.verify(methodInvoker -> {
User user = methodInvoker.invoke("findAnnotatedQueryByUsername", "yoda").onBean("aotUserRepository");
assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
});
}
@Test
void testAnnotatedCount() {
generated.verify(methodInvoker -> {
Long value = methodInvoker.invoke("countAnnotatedQueryByLastname", "Skywalker").onBean("aotUserRepository");
assertThat(value).isEqualTo(2L);
});
}
@Test
void testAnnotatedFinderReturningListWithQuery() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
});
}
@Test
void testAnnotatedMultilineFinderWithQuery() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
});
}
@Test
void testAnnotatedFinderWithQueryAndLimit() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testAnnotatedFinderWithQueryAndSort() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("username"))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
});
}
@Test
void testAnnotatedFinderWithQueryLimitAndSort() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("username"))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@Test
void testAnnotatedFinderReturningListWithPageable() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker
.invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("username")))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@Test
void testAnnotatedFinderReturningPage() {
generated.verify(methodInvoker -> {
Page<User> page = methodInvoker
.invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username")))
.onBean("aotUserRepository");
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@Test
void testAnnotatedFinderReturningSlice() {
generated.verify(methodInvoker -> {
Slice<User> slice = methodInvoker
.invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username")))
.onBean("aotUserRepository");
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
});
}
@ParameterizedTest
@ValueSource(strings = { "deleteByUsername", "deleteAnnotatedQueryByUsername" })
void testDeleteSingle(String methodName) {
generated.verify(methodInvoker -> {
User result = methodInvoker.invoke(methodName, "yoda").onBean("aotUserRepository");
assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
});
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L);
}
@ParameterizedTest
@ValueSource(strings = { "deleteByLastnameStartingWith", "deleteAnnotatedQueryByLastnameStartingWith" })
void testDerivedDeleteMultipleReturningDeleteCount(String methodName) {
generated.verify(methodInvoker -> {
Long result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository");
assertThat(result).isEqualTo(4L);
});
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
}
@ParameterizedTest
@ValueSource(strings = { "deleteUsersByLastnameStartingWith", "deleteUsersAnnotatedQueryByLastnameStartingWith" })
void testDerivedDeleteMultipleReturningDeleted(String methodName) {
generated.verify(methodInvoker -> {
List<User> result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository");
assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
});
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
}
@Test
void testDerivedFinderWithAnnotatedSort() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findWithAnnotatedSortByLastnameStartingWith", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
});
}
@Test
void testDerivedFinderWithAnnotatedFieldsProjection() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findWithAnnotatedFieldsProjectionByLastnameStartingWith", "S")
.onBean("aotUserRepository");
assertThat(users).allMatch(
user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null);
});
}
@Test
void testReadPreferenceAppliedToQuery() {
generated.verify(methodInvoker -> {
// check if it fails when trying to parse the read preference to indicate it would get applied
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> methodInvoker.invoke("findWithReadPreferenceByUsername", "S").onBean("aotUserRepository"))
.withMessageContaining("No match for read preference");
});
}
@Test
void testDerivedFinderReturningListOfProjections() {
generated.verify(methodInvoker -> {
List<UserProjection> users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke",
"vader");
});
}
@Test
void testDerivedFinderReturningPageOfProjections() {
generated.verify(methodInvoker -> {
Page<UserProjection> users = methodInvoker
.invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username")))
.onBean("aotUserRepository");
assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo");
});
}
private static void initUsers() {
Document luke = Document.parse("""
{
"_id": "id-1",
"username": "luke",
"first_name": "Luke",
"last_name": "Skywalker",
"posts": [
{
"message": "I have a bad feeling about this.",
"date": {
"$date": "2025-01-15T12:50:33.855Z"
}
}
],
"_class": "example.springdata.aot.User"
}""");
Document leia = Document.parse("""
{
"_id": "id-2",
"username": "leia",
"first_name": "Leia",
"last_name": "Organa",
"_class": "example.springdata.aot.User"
}""");
Document han = Document.parse("""
{
"_id": "id-3",
"username": "han",
"first_name": "Han",
"last_name": "Solo",
"posts": [
{
"message": "It's the ship that made the Kessel Run in less than 12 Parsecs.",
"date": {
"$date": "2025-01-15T13:30:33.855Z"
}
}
],
"_class": "example.springdata.aot.User"
}""");
Document chwebacca = Document.parse("""
{
"_id": "id-4",
"username": "chewbacca",
"_class": "example.springdata.aot.User"
}""");
Document yoda = Document.parse(
"""
{
"_id": "id-5",
"username": "yoda",
"posts": [
{
"message": "Do. Or do not. There is no try.",
"date": {
"$date": "2025-01-15T13:09:33.855Z"
}
},
{
"message": "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered.",
"date": {
"$date": "2025-01-15T13:53:33.855Z"
}
}
]
}""");
Document vader = Document.parse("""
{
"_id": "id-6",
"username": "vader",
"first_name": "Anakin",
"last_name": "Skywalker",
"posts": [
{
"message": "I am your father",
"date": {
"$date": "2025-01-15T13:46:33.855Z"
}
}
]
}""");
Document kylo = Document.parse("""
{
"_id": "id-7",
"username": "kylo",
"first_name": "Ben",
"last_name": "Solo"
}
""");
client.getDatabase(DB_NAME).getCollection("user")
.insertMany(List.of(luke, leia, han, chwebacca, yoda, vader, kylo));
}
static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) {
return new GeneratedContextBuilder(generationContext);
}
static class GeneratedContextBuilder implements Verifyer {
TestGenerationContext generationContext;
Map<String, BeanDefinition> beanDefinitions = new LinkedHashMap<>();
Lazy<DefaultListableBeanFactory> lazyFactory;
public GeneratedContextBuilder(TestGenerationContext generationContext) {
this.generationContext = generationContext;
this.lazyFactory = Lazy.of(() -> {
DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory();
TestCompiler.forSystem().with(generationContext).compile(compiled -> {
freshBeanFactory.setBeanClassLoader(compiled.getClassLoader());
for (Entry<String, BeanDefinition> entry : beanDefinitions.entrySet()) {
freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue());
}
});
return freshBeanFactory;
});
}
GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) {
this.beanDefinitions.put(name, beanDefinition);
return this;
}
public Verifyer verify(Consumer<GeneratedContext> methodInvoker) {
methodInvoker.accept(new GeneratedContext(lazyFactory));
return this;
}
}
interface Verifyer {
Verifyer verify(Consumer<GeneratedContext> methodInvoker);
}
static class GeneratedContext {
private Supplier<DefaultListableBeanFactory> delegate;
public GeneratedContext(Supplier<DefaultListableBeanFactory> defaultListableBeanFactory) {
this.delegate = defaultListableBeanFactory;
}
InvocationBuilder invoke(String method, Object... arguments) {
return new InvocationBuilder() {
@Override
public <T> T onBean(String beanName) {
Object bean = delegate.get().getBean(beanName);
return ReflectionTestUtils.invokeMethod(bean, method, arguments);
}
};
}
interface InvocationBuilder {
<T> T onBean(String beanName);
}
}
}

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

@ -0,0 +1,127 @@ @@ -0,0 +1,127 @@
/*
* 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.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.ReflectionUtils;
/**
* Test Configuration Support Class for generated AOT Repository Fragments based on a Repository Interface.
* <p>
* This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT
* fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method
* invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy.
*
* @author Christoph Strobl
*/
public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor {
private final Class<?> repositoryInterface;
private final TestMongoAotRepositoryContext repositoryContext;
public AotFragmentTestConfigurationSupport(Class<?> repositoryInterface) {
this.repositoryInterface = repositoryInterface;
this.repositoryContext = new TestMongoAotRepositoryContext(repositoryInterface, null);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface);
new MongoRepositoryContributor(repositoryContext).contribute(generationContext);
AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
.genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") //
.addConstructorArgReference("mongoOperations") //
.addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition();
TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> {
beanFactory.setBeanClassLoader(compiled.getClassLoader());
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository);
});
BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> {
Object fragment = beanFactory.getBean("fragment");
Object proxy = getFragmentFacadeProxy(fragment);
return repositoryInterface.cast(proxy);
}).getBeanDefinition();
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade);
}
private Object getFragmentFacadeProxy(Object fragment) {
return Proxy.newProxyInstance(repositoryInterface.getClassLoader(), new Class<?>[] { repositoryInterface },
(p, method, args) -> {
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));
}
try {
return target.invoke(fragment, args);
} catch (ReflectiveOperationException e) {
ReflectionUtils.handleReflectionException(e);
}
return null;
});
}
private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext(
TestMongoAotRepositoryContext repositoryContext) {
return new RepositoryFactoryBeanSupport.FragmentCreationContext() {
@Override
public RepositoryMetadata getRepositoryMetadata() {
return repositoryContext.getRepositoryInformation();
}
@Override
public ValueExpressionDelegate getValueExpressionDelegate() {
return ValueExpressionDelegate.create();
}
@Override
public ProjectionFactory getProjectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
};
}
}

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

@ -0,0 +1,650 @@ @@ -0,0 +1,650 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import example.aot.User;
import example.aot.UserProjection;
import example.aot.UserRepository;
import example.aot.UserRepository.UserAggregate;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
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.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
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;
/**
* @author Christoph Strobl
*/
@ExtendWith(MongoClientExtension.class)
@SpringJUnitConfig(classes = MongoRepositoryContributorTests.JpaRepositoryContributorConfiguration.class)
public class MongoRepositoryContributorTests {
private static final String DB_NAME = "aot-repo-tests";
@Client static MongoClient client;
@Autowired UserRepository fragment;
@Configuration
static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
public JpaRepositoryContributorConfiguration() {
super(UserRepository.class);
}
@Bean
MongoOperations mongoOperations() {
return new MongoTemplate(client, DB_NAME);
}
}
@BeforeEach
void beforeEach() {
MongoTestUtils.flushCollection(DB_NAME, "user", client);
initUsers();
}
@Test
void testFindDerivedFinderSingleEntity() {
User user = fragment.findOneByUsername("yoda");
assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
}
@Test
void testFindDerivedFinderOptionalEntity() {
Optional<User> user = fragment.findOptionalOneByUsername("yoda");
assertThat(user).isNotNull().containsInstanceOf(User.class)
.hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda"));
}
@Test
void testDerivedCount() {
Long value = fragment.countUsersByLastname("Skywalker");
assertThat(value).isEqualTo(2L);
}
@Test
void testDerivedExists() {
assertThat(fragment.existsUserByLastname("Skywalker")).isTrue();
}
@Test
void testDerivedFinderWithoutArguments() {
List<User> users = fragment.findUserNoArgumentsBy();
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
}
@Test
void testCountWorksAsExpected() {
Long value = fragment.countUsersByLastname("Skywalker");
assertThat(value).isEqualTo(2L);
}
@Test
void testDerivedFinderReturningList() {
List<User> users = fragment.findByLastnameStartingWith("S");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han");
}
@Test
void testEndingWith() {
List<User> users = fragment.findByLastnameEndsWith("er");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader");
}
@Test
void testLike() {
List<User> users = fragment.findByFirstnameLike("ei");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("leia");
}
@Test
void testNotLike() {
List<User> users = fragment.findByFirstnameNotLike("ei");
assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("leia");
}
@Test
void testIn() {
List<User> users = fragment.findByUsernameIn(List.of("chewbacca", "kylo"));
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("chewbacca", "kylo");
}
@Test
void testNotIn() {
List<User> users = fragment.findByUsernameNotIn(List.of("chewbacca", "kylo"));
assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("chewbacca", "kylo");
}
@Test
void testAnd() {
List<User> users = fragment.findByFirstnameAndLastname("Han", "Solo");
assertThat(users).extracting(User::getUsername).containsExactly("han");
}
@Test
void testOr() {
List<User> users = fragment.findByFirstnameOrLastname("Han", "Skywalker");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "vader", "luke");
}
@Test
void testBetween() {
List<User> users = fragment.findByVisitsBetween(10, 100);
assertThat(users).extracting(User::getUsername).containsExactly("vader");
}
@Test
void testTimeValue() {
List<User> users = fragment.findByLastSeenGreaterThan(Instant.parse("2025-01-01T00:00:00.000Z"));
assertThat(users).extracting(User::getUsername).containsExactly("luke");
}
@Test
void testNot() {
List<User> users = fragment.findByLastnameNot("Skywalker");
assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("luke", "vader");
}
@Test
void testExistsCriteria() {
List<User> users = fragment.findByVisitsExists(false);
assertThat(users).extracting(User::getUsername).contains("kylo");
}
@Test
void testLimitedDerivedFinder() {
List<User> users = fragment.findTop2ByLastnameStartingWith("S");
assertThat(users).hasSize(2);
}
@Test
void testSortedDerivedFinder() {
List<User> users = fragment.findByLastnameStartingWithOrderByUsername("S");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
}
@Test
void testDerivedFinderWithLimitArgument() {
List<User> users = fragment.findByLastnameStartingWith("S", Limit.of(2));
assertThat(users).hasSize(2);
}
@Test
void testDerivedFinderWithSort() {
List<User> users = fragment.findByLastnameStartingWith("S", Sort.by("username"));
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
}
@Test
void testDerivedFinderWithSortAndLimit() {
List<User> users = fragment.findByLastnameStartingWith("S", Sort.by("username"), Limit.of(2));
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testDerivedFinderReturningListWithPageable() {
List<User> users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username")));
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testDerivedFinderReturningPage() {
Page<User> page = fragment.findPageOfUsersByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username")));
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testDerivedFinderReturningSlice() {
Slice<User> slice = fragment.findSliceOfUserByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username")));
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testAnnotatedFinderReturningSingleValueWithQuery() {
User user = fragment.findAnnotatedQueryByUsername("yoda");
assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
}
@Test
void testAnnotatedCount() {
Long value = fragment.countAnnotatedQueryByLastname("Skywalker");
assertThat(value).isEqualTo(2L);
}
@Test
void testAnnotatedFinderReturningListWithQuery() {
List<User> users = fragment.findAnnotatedQueryByLastname("S");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
}
@Test
void testAnnotatedMultilineFinderWithQuery() {
List<User> users = fragment.findAnnotatedMultilineQueryByLastname("S");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
}
@Test
void testAnnotatedFinderWithQueryAndLimit() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2));
assertThat(users).hasSize(2);
}
@Test
void testAnnotatedFinderWithQueryAndSort() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", Sort.by("username"));
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
}
@Test
void testAnnotatedFinderWithQueryLimitAndSort() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("username"));
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testAnnotatedFinderReturningListWithPageable() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("username")));
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testAnnotatedFinderReturningPage() {
Page<User> page = fragment.findAnnotatedQueryPageOfUsersByLastname("S", PageRequest.of(0, 2, Sort.by("username")));
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testAnnotatedFinderReturningSlice() {
Slice<User> slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S",
PageRequest.of(0, 2, Sort.by("username")));
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo");
}
@Test
void testDeleteSingle() {
User result = fragment.deleteByUsername("yoda");
assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L);
}
@Test
void testDeleteSingleAnnotatedQuery() {
User result = fragment.deleteAnnotatedQueryByUsername("yoda");
assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda");
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L);
}
@Test
void testDerivedDeleteMultipleReturningDeleteCount() {
Long result = fragment.deleteByLastnameStartingWith("S");
assertThat(result).isEqualTo(4L);
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
}
@Test
void testDerivedDeleteMultipleReturningDeleteCountAnnotatedQuery() {
Long result = fragment.deleteAnnotatedQueryByLastnameStartingWith("S");
assertThat(result).isEqualTo(4L);
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
}
@Test
void testDerivedDeleteMultipleReturningDeleted() {
List<User> result = fragment.deleteUsersByLastnameStartingWith("S");
assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
}
@Test
void testDerivedDeleteMultipleReturningDeletedAnnotatedQuery() {
List<User> result = fragment.deleteUsersAnnotatedQueryByLastnameStartingWith("S");
assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L);
}
@Test
void testDerivedFinderWithAnnotatedSort() {
List<User> users = fragment.findWithAnnotatedSortByLastnameStartingWith("S");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
}
@Test
void testDerivedFinderWithAnnotatedFieldsProjection() {
List<User> users = fragment.findWithAnnotatedFieldsProjectionByLastnameStartingWith("S");
assertThat(users).allMatch(
user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null);
}
@Test
void testReadPreferenceAppliedToQuery() {
// check if it fails when trying to parse the read preference to indicate it would get applied
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> fragment.findWithReadPreferenceByUsername("S"))
.withMessageContaining("No match for read preference");
}
@Test
void testDerivedFinderReturningListOfProjections() {
List<UserProjection> users = fragment.findUserProjectionByLastnameStartingWith("S");
assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
}
@Test
void testDerivedFinderReturningPageOfProjections() {
Page<UserProjection> users = fragment.findUserProjectionByLastnameStartingWith("S",
PageRequest.of(0, 2, Sort.by("username")));
assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo");
}
@Test
void testUpdateWithDerivedQuery() {
int modifiedCount = fragment.findUserAndIncrementVisitsByLastname("Organa", 42);
assertThat(modifiedCount).isOne();
assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits",
Integer.class)).isEqualTo(42);
}
@Test
void testUpdateWithAnnotatedQuery() {
int modifiedCount = fragment.updateAllByLastname("Organa", 42);
assertThat(modifiedCount).isOne();
assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits",
Integer.class)).isEqualTo(42);
}
@Test
void testAggregationPipelineUpdate() {
fragment.findAndIncrementVisitsViaPipelineByLastname("Organa", 42);
assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits",
Integer.class)).isEqualTo(42);
}
@Test
void testAggregationWithExtractedSimpleResults() {
List<String> allLastnames = fragment.findAllLastnames();
assertThat(allLastnames).containsExactlyInAnyOrder("Skywalker", "Solo", "Organa", "Solo", "Skywalker");
}
@Test
void testAggregationWithProjectedResults() {
List<UserAggregate> allLastnames = fragment.groupByLastnameAnd("first_name");
assertThat(allLastnames).containsExactlyInAnyOrder(//
new UserAggregate("Skywalker", List.of("Anakin", "Luke")), //
new UserAggregate("Organa", List.of("Leia")), //
new UserAggregate("Solo", List.of("Han", "Ben")));
}
@Test
void testAggregationWithProjectedResultsLimitedByPageable() {
List<UserAggregate> allLastnames = fragment.groupByLastnameAnd("first_name", PageRequest.of(1, 1, Sort.by("_id")));
assertThat(allLastnames).containsExactly(//
new UserAggregate("Skywalker", List.of("Anakin", "Luke")) //
);
}
@Test
void testAggregationWithProjectedResultsAsPage() {
Slice<UserAggregate> allLastnames = fragment.groupByLastnameAndReturnPage("first_name",
PageRequest.of(1, 1, Sort.by("_id")));
assertThat(allLastnames.hasPrevious()).isTrue();
assertThat(allLastnames.hasNext()).isTrue();
assertThat(allLastnames.getContent()).containsExactly(//
new UserAggregate("Skywalker", List.of("Anakin", "Luke")) //
);
}
@Test
void testAggregationWithProjectedResultsWrappedInAggregationResults() {
AggregationResults<UserAggregate> allLastnames = fragment.groupByLastnameAndAsAggregationResults("first_name");
assertThat(allLastnames.getMappedResults()).containsExactlyInAnyOrder(//
new UserAggregate("Skywalker", List.of("Anakin", "Luke")), //
new UserAggregate("Organa", List.of("Leia")), //
new UserAggregate("Solo", List.of("Han", "Ben")));
}
@Test
void testAggregationWithSingleResultExtraction() {
assertThat(fragment.sumPosts()).isEqualTo(5);
}
@Test
void testAggregationWithHint() {
assertThatException().isThrownBy(() -> fragment.findAllLastnamesUsingIndex())
.withMessageContaining("hint provided does not correspond to an existing index");
}
@Test
void testAggregationWithReadPreference() {
assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithReadPreference())
.withMessageContaining("No match for read preference");
}
@Test
void testAggregationWithCollation() {
assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithCollation())
.withMessageContaining("'locale' is invalid");
}
private static void initUsers() {
Document luke = Document.parse("""
{
"_id": "id-1",
"username": "luke",
"first_name": "Luke",
"last_name": "Skywalker",
"visits" : 2,
"lastSeen" : {
"$date": "2025-04-01T00:00:00.000Z"
},
"posts": [
{
"message": "I have a bad feeling about this.",
"date": {
"$date": "2025-01-15T12:50:33.855Z"
}
}
],
"_class": "example.springdata.aot.User"
}""");
Document leia = Document.parse("""
{
"_id": "id-2",
"username": "leia",
"first_name": "Leia",
"last_name": "Organa",
"_class": "example.springdata.aot.User"
}""");
Document han = Document.parse("""
{
"_id": "id-3",
"username": "han",
"first_name": "Han",
"last_name": "Solo",
"posts": [
{
"message": "It's the ship that made the Kessel Run in less than 12 Parsecs.",
"date": {
"$date": "2025-01-15T13:30:33.855Z"
}
}
],
"_class": "example.springdata.aot.User"
}""");
Document chwebacca = Document.parse("""
{
"_id": "id-4",
"username": "chewbacca",
"lastSeen" : {
"$date": "2025-01-01T00:00:00.000Z"
},
"_class": "example.springdata.aot.User"
}""");
Document yoda = Document.parse(
"""
{
"_id": "id-5",
"username": "yoda",
"visits" : 1000,
"posts": [
{
"message": "Do. Or do not. There is no try.",
"date": {
"$date": "2025-01-15T13:09:33.855Z"
}
},
{
"message": "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered.",
"date": {
"$date": "2025-01-15T13:53:33.855Z"
}
}
]
}""");
Document vader = Document.parse("""
{
"_id": "id-6",
"username": "vader",
"first_name": "Anakin",
"last_name": "Skywalker",
"visits" : 50,
"posts": [
{
"message": "I am your father",
"date": {
"$date": "2025-01-15T13:46:33.855Z"
}
}
]
}""");
Document kylo = Document.parse("""
{
"_id": "id-7",
"username": "kylo",
"first_name": "Ben",
"last_name": "Solo"
}
""");
client.getDatabase(DB_NAME).getCollection("user")
.insertMany(List.of(luke, leia, han, chwebacca, yoda, vader, kylo));
}
}

39
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java → spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java

@ -1,19 +1,3 @@ @@ -1,19 +1,3 @@
/*
* 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.
*
@ -21,7 +5,7 @@ @@ -21,7 +5,7 @@
* 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
* 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,
@ -29,11 +13,13 @@ @@ -29,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.aot.generated;
package org.springframework.data.mongodb.repository.aot;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryInformation;
@ -41,15 +27,11 @@ import org.springframework.data.repository.core.RepositoryMetadata; @@ -41,15 +27,11 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 2025/01
*/
class StubRepositoryInformation implements RepositoryInformation {
private final RepositoryMetadata metadata;
@ -124,11 +106,15 @@ class StubRepositoryInformation implements RepositoryInformation { @@ -124,11 +106,15 @@ class StubRepositoryInformation implements RepositoryInformation {
@Override
public boolean isQueryMethod(Method method) {
return false;
if (isBaseClassMethod(method)) {
return false;
}
return true;
}
@Override
public Streamable<Method> getQueryMethods() {
public List<Method> getQueryMethods() {
return null;
}
@ -141,4 +127,9 @@ class StubRepositoryInformation implements RepositoryInformation { @@ -141,4 +127,9 @@ class StubRepositoryInformation implements RepositoryInformation {
public Method getTargetClassMethod(Method method) {
return null;
}
@Override
public RepositoryComposition getRepositoryComposition() {
return baseComposition;
}
}

27
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java → spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java

@ -1,19 +1,3 @@ @@ -1,19 +1,3 @@
/*
* 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.
*
@ -21,7 +5,7 @@ @@ -21,7 +5,7 @@
* 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
* 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,
@ -29,13 +13,14 @@ @@ -29,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.aot.generated;
package org.springframework.data.mongodb.repository.aot;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.test.tools.ClassFile;
@ -46,18 +31,16 @@ import org.springframework.data.mongodb.core.mapping.Document; @@ -46,18 +31,16 @@ import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 2025/01
*/
public class TestMongoAotRepositoryContext implements AotRepositoryContext {
class TestMongoAotRepositoryContext implements AotRepositoryContext {
private final StubRepositoryInformation repositoryInformation;
private final Environment environment = new StandardEnvironment();
public TestMongoAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
TestMongoAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
}

14
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java

@ -51,6 +51,7 @@ import org.springframework.data.expression.ValueExpressionParser; @@ -51,6 +51,7 @@ import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
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.ExecutableUpdateOperation.TerminatingUpdate;
import org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithQuery;
@ -104,6 +105,7 @@ class AbstractMongoQueryUnitTests { @@ -104,6 +105,7 @@ class AbstractMongoQueryUnitTests {
@Mock UpdateWithQuery updateWithQuery;
@Mock UpdateWithUpdate updateWithUpdate;
@Mock TerminatingUpdate terminatingUpdate;
@Mock ExecutableRemove executableRemove;
@Mock BasicMongoPersistentEntity<?> persitentEntityMock;
@Mock MongoMappingContext mappingContextMock;
@Mock DeleteResult deleteResultMock;
@ -130,8 +132,9 @@ class AbstractMongoQueryUnitTests { @@ -130,8 +132,9 @@ class AbstractMongoQueryUnitTests {
doReturn(executableUpdate).when(mongoOperationsMock).update(any());
doReturn(updateWithQuery).when(executableUpdate).matching(any(Query.class));
doReturn(terminatingUpdate).when(updateWithQuery).apply(any(UpdateDefinition.class));
when(mongoOperationsMock.remove(any(), any(), anyString())).thenReturn(deleteResultMock);
doReturn(executableRemove).when(mongoOperationsMock).remove(any());
doReturn(executableRemove).when(executableRemove).matching(any(Query.class));
when(executableRemove.all()).thenReturn(deleteResultMock);
when(mongoOperationsMock.updateMulti(any(), any(), any(), anyString())).thenReturn(updateResultMock);
}
@ -140,8 +143,7 @@ class AbstractMongoQueryUnitTests { @@ -140,8 +143,7 @@ class AbstractMongoQueryUnitTests {
createQueryForMethod("deletePersonByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" });
verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons"));
verify(mongoOperationsMock, times(0)).find(any(), any(), any());
verify(executableRemove, times(1)).all();
}
@Test // DATAMONGO-566, DATAMONGO-1040
@ -149,7 +151,7 @@ class AbstractMongoQueryUnitTests { @@ -149,7 +151,7 @@ class AbstractMongoQueryUnitTests {
createQueryForMethod("deleteByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" });
verify(mongoOperationsMock, times(1)).findAllAndRemove(any(), eq(Person.class), eq("persons"));
verify(executableRemove, times(1)).findAndRemove();
}
@Test // DATAMONGO-566
@ -171,7 +173,7 @@ class AbstractMongoQueryUnitTests { @@ -171,7 +173,7 @@ class AbstractMongoQueryUnitTests {
query.setDeleteQuery(true);
assertThat(query.execute(new Object[] { "fake" })).isEqualTo(100L);
verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons"));
verify(executableRemove, times(1)).all();
}
@Test // DATAMONGO-957

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

@ -15,20 +15,23 @@ @@ -15,20 +15,23 @@
*/
package org.springframework.data.mongodb.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
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;
@ -41,6 +44,7 @@ import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableF @@ -41,6 +44,7 @@ import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableF
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind;
import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFindNear;
import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@ -78,6 +82,7 @@ class MongoQueryExecutionUnitTests { @@ -78,6 +82,7 @@ class MongoQueryExecutionUnitTests {
@Mock FindWithQuery<Object> operationMock;
@Mock TerminatingFind<Object> terminatingMock;
@Mock TerminatingFindNear<Object> terminatingGeoMock;
@Mock ExecutableRemove<Object> removeMock;
@Mock DbRefResolver dbRefResolver;
private Point POINT = new Point(10, 20);
@ -183,38 +188,36 @@ class MongoQueryExecutionUnitTests { @@ -183,38 +188,36 @@ class MongoQueryExecutionUnitTests {
@Test // DATAMONGO-2351
void acknowledgedDeleteReturnsDeletedCount() {
doReturn(removeMock).when(removeMock).matching(any(Query.class));
when(removeMock.all()).thenReturn(DeleteResult.acknowledged(10));
Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class);
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString()))
.thenReturn(DeleteResult.acknowledged(10));
assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(10L);
assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(10L);
}
@Test // DATAMONGO-2351
void unacknowledgedDeleteReturnsZeroDeletedCount() {
doReturn(removeMock).when(removeMock).matching(any(Query.class));
when(removeMock.all()).thenReturn(DeleteResult.unacknowledged());
Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class);
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString()))
.thenReturn(DeleteResult.unacknowledged());
assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(0L);
assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(0L);
}
@Test // DATAMONGO-1997
void deleteExecutionWithEntityReturnTypeTriggersFindAndRemove() {
Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class);
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
Person person = new Person();
doReturn(removeMock).when(removeMock).matching(any(Query.class));
when(removeMock.findAndRemove()).thenReturn(List.of(person));
when(mongoOperationsMock.findAndRemove(any(Query.class), any(Class.class), anyString())).thenReturn(person);
Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class);
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(person);
assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(person);
}
interface PersonRepository extends Repository<Person, Long> {

8
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java → spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
package org.springframework.data.mongodb.util;
import static org.assertj.core.api.Assertions.assertThat;
@ -25,10 +25,12 @@ import org.junit.jupiter.api.BeforeEach; @@ -25,10 +25,12 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link SpringJsonWriter}.
*
* @author Christoph Strobl
* @since 2025/01
* @since 5.0
*/
public class SpringJsonWriterUnitTests {
class SpringJsonWriterUnitTests {
StringBuffer buffer;
SpringJsonWriter writer;

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

@ -18,7 +18,9 @@ @@ -18,7 +18,9 @@
<appender-ref ref="no-op" />
</logger>
<logger name="org.springframework.data.mongodb.test.util" level="info"/>
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="trace" />
<!-- AOT Code Generation -->
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="info" />
<root level="error">
<appender-ref ref="console" />

1
src/main/antora/modules/ROOT/nav.adoc

@ -53,6 +53,7 @@ @@ -53,6 +53,7 @@
** xref:mongodb/repositories/cdi-integration.adoc[]
** xref:repositories/query-keywords-reference.adoc[]
** xref:repositories/query-return-types-reference.adoc[]
** xref:mongodb/aot.adoc[]
// Observability
* xref:observability/observability.adoc[]

88
src/main/antora/modules/ROOT/pages/mongodb/aot.adoc

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
= Ahead of Time Optimizations
This chapter covers Spring Data's Ahead of Time (AOT) optimizations that build upon {spring-framework-docs}/core/aot.html[Spring's Ahead of Time Optimizations].
[[aot.hints]]
== Runtime Hints
Running an application as a native image requires additional information compared to a regular JVM runtime.
Spring Data contributes {spring-framework-docs}/core/aot.html#aot.hints[Runtime Hints] during AOT processing for native image usage.
These are in particular hints for:
* Auditing
* `ManagedTypes` to capture the outcome of class-path scans
* Repositories
** Reflection hints for entities, return types, and Spring Data annotations
** Repository fragments
** Querydsl `Q` classes
** Kotlin Coroutine support
* Web support (Jackson Hints for `PagedModel`)
[[aot.repositories]]
== Ahead of Time Repositories
AOT Repositories are an extension to AOT processing by pre-generating eligible query method implementations.
Query methods are opaque to developers regarding their underlying queries being executed in a query method call.
AOT repositories contribute query method implementations based on derived or annotated queries, updates or aggregations that are known at build-time.
This optimization moves query method processing from runtime to build-time, which can lead to a significant bootstrap performance improvement as query methods do not need to be analyzed reflectively upon each application start.
The resulting AOT repository fragment follows the naming scheme of `<Repository FQCN>Impl__Aot` and is placed in the same package as the repository interface.
You can find all queries in their MQL form for generated repository query methods.
[TIP]
====
`spring.aot.repositories.enabled` property needs to be set to `true` for repository fragment code generation.
====
[NOTE]
====
Consider AOT repository classes an internal optimization.
Do not use them directly in your code as generation and implementation details may change in future releases.
====
=== Running with AOT Repositories
AOT is a mandatory step to transform a Spring application to a native executable, so it is automatically enabled when running in this mode.
It is also possible to use those optimizations on the JVM by setting the `spring.aot.enabled` and `spring.aot.repositories.enabled` System properties to `true`.
AOT repositories contribute configuration changes to the actual repository bean registration to register the generated repository fragment.
[NOTE]
====
When AOT optimizations are included, some decisions that have been taken at build-time are hard-coded in the application setup.
For instance, profiles that have been enabled at build-time are automatically enabled at runtime as well.
Also, the Spring Data module implementing a repository is fixed.
Changing the implementation requires AOT re-processing.
====
=== Eligible Methods in Data MongoDB
AOT repositories filter methods that are eligible for AOT processing.
These are typically all query methods that are not backed by an xref:repositories/custom-implementations.adoc[implementation fragment].
**Supported Features**
* Derived `find`, `count`, `exists` and `delete` methods
* Query methods annotated with `@Query` (excluding those containing SpEL)
* Methods annotated with `@Aggregation`
* Methods using `@Update`
* `@Hint` & `@ReadPreference` support
* `Page`, `Slice`, and `Optional` return types
* DTO Projections
**Limitations**
* `@Meta` annotations are not evaluated.
* Queries / Aggregations / Updates containing `SpEL` cannot be generated.
* Limited `Collation` detection.
* Reserved parameter names (must not be used in method signature) `finder`, `filterQuery`, `countQuery`, `deleteQuery`, `remover` `updateDefinition`, `aggregation`, `aggregationPipeline`, `aggregationUpdate`, `aggregationOptions`, `updater`, `results`, `fields`.
**Excluded methods**
* `CrudRepository` and other base interface methods
* Querydsl and Query by Example methods
* Methods whose implementation would be overly complex
* Query Methods obtaining MQL from a file
** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination)
** Dynamic projections
** Geospatial Queries
Loading…
Cancel
Save