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