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
This commit is contained in:
committed by
Mark Paluch
parent
2aac1a59be
commit
6c6438ec21
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
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 {
|
||||
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 {
|
||||
*/
|
||||
@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 {
|
||||
*/
|
||||
@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 {
|
||||
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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
public Placeholder(Object value) {
|
||||
this.value = value;
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
Placeholder(Object value) {
|
||||
this.expression = value;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
return value;
|
||||
return expression;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
}
|
||||
}
|
||||
|
||||
@NullUnmarked
|
||||
static class PlaceholderParameterAccessor implements MongoParameterAccessor {
|
||||
|
||||
private final List<Placeholder> placeholders;
|
||||
@@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return ((List) placeholders).iterator();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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 @@
|
||||
* 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 @@
|
||||
* 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 {
|
||||
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 {
|
||||
this.raw = query;
|
||||
}
|
||||
|
||||
public StringQuery forCount() {
|
||||
this.executionType = ExecutionType.COUNT;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getQueryString() {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadPreference getReadPreference() {
|
||||
public @Nullable ReadPreference getReadPreference() {
|
||||
return delegate.getReadPreference();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getHint() {
|
||||
public @Nullable String getHint() {
|
||||
return delegate.getHint();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery<?> operation) {
|
||||
|
||||
if (isDeleteQuery()) {
|
||||
return new DeleteExecution(operations, method);
|
||||
return new DeleteExecution<>(executableRemove, method);
|
||||
}
|
||||
|
||||
if (method.isModifyingQuery()) {
|
||||
|
||||
@@ -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> {
|
||||
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> {
|
||||
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> {
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
} 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 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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.
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
return doFindAnnotation(Query.class);
|
||||
}
|
||||
|
||||
TypeInformation<?> getReturnType() {
|
||||
public TypeInformation<?> getReturnType() {
|
||||
return TypeInformation.fromReturnTypeOf(method);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -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;
|
||||
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 {
|
||||
}
|
||||
|
||||
private static class JsonContext {
|
||||
|
||||
private final JsonContext parentContext;
|
||||
private final JsonContextType contextType;
|
||||
private boolean hasElements;
|
||||
@@ -450,6 +456,9 @@ public class SpringJsonWriter implements BsonWriter {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param placeholder
|
||||
*/
|
||||
public void writePlaceholder(String placeholder) {
|
||||
write(placeholder);
|
||||
}
|
||||
@@ -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 @@
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 2025/01
|
||||
*/
|
||||
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 {
|
||||
public void setLastSeen(Instant lastSeen) {
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
|
||||
public Long getVisits() {
|
||||
return visits;
|
||||
}
|
||||
|
||||
public void setVisits(Long visits) {
|
||||
this.visits = visits;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import java.time.Instant;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 2025/01
|
||||
*/
|
||||
public interface UserProjection {
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
*/
|
||||
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> {
|
||||
|
||||
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> {
|
||||
// TODO: Streaming
|
||||
// TODO: Scrolling
|
||||
// TODO: GeoQueries
|
||||
// TODO: TextSearch
|
||||
|
||||
/* Annotated Queries */
|
||||
|
||||
@@ -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> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
* 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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
* 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 @@
|
||||
* 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;
|
||||
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 {
|
||||
|
||||
@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 {
|
||||
public Method getTargetClassMethod(Method method) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryComposition getRepositoryComposition() {
|
||||
return baseComposition;
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
* 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 @@
|
||||
* 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;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@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 {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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 {
|
||||
@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 {
|
||||
@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() {
|
||||
|
||||
Person person = new Person();
|
||||
doReturn(removeMock).when(removeMock).matching(any(Query.class));
|
||||
when(removeMock.findAndRemove()).thenReturn(List.of(person));
|
||||
|
||||
Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class);
|
||||
MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context);
|
||||
|
||||
Person person = new Person();
|
||||
|
||||
when(mongoOperationsMock.findAndRemove(any(Query.class), any(Class.class), anyString())).thenReturn(person);
|
||||
|
||||
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> {
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
Normal file
88
src/main/antora/modules/ROOT/pages/mongodb/aot.adoc
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user