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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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