Browse Source

Update AOT fragment generation to align with reflective behavior.

Closes: #5027
Original pull request: #5038
pull/5040/head
Christoph Strobl 5 months ago committed by Mark Paluch
parent
commit
72d9361706
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 13
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  2. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java
  3. 50
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java
  4. 55
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java
  5. 69
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java
  6. 25
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java
  7. 88
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java
  8. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java
  9. 7
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java
  10. 118
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java
  11. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java
  12. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
  13. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java
  14. 85
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
  15. 21
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
  16. 33
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java
  17. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java
  18. 11
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
  19. 2
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java
  20. 13
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java
  21. 12
      src/main/antora/modules/ROOT/pages/mongodb/aot.adoc

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

@ -111,9 +111,11 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjection;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.Lazy; import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals; import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Contract; import org.springframework.lang.Contract;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -2272,8 +2274,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
<T, O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<T> outputType, <T, O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<T> outputType,
QueryResultConverter<? super T, ? extends O> resultConverter, AggregationOperationContext context) { QueryResultConverter<? super T, ? extends O> resultConverter, AggregationOperationContext context) {
DocumentCallback<O> callback = new QueryResultConverterCallback<>(resultConverter, final DocumentCallback<O> callback;
if(aggregation instanceof TypedAggregation<?> ta && outputType.isInterface()) {
EntityProjection<T, ?> projection = operations.introspectProjection(outputType, ta.getInputType());
ProjectingReadCallback cb = new ProjectingReadCallback(mongoConverter, projection, collectionName);
callback = new QueryResultConverterCallback<>(resultConverter,
cb);
} else {
callback = new QueryResultConverterCallback<>(resultConverter,
new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); new ReadDocumentCallback<>(mongoConverter, outputType, collectionName));
}
AggregationOptions options = aggregation.getOptions(); AggregationOptions options = aggregation.getOptions();
AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext); AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext);

3
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java

@ -377,6 +377,9 @@ class QueryOperations {
mappedFields = queryMapper.getMappedFields(fields, entity); mappedFields = queryMapper.getMappedFields(fields, entity);
} else { } else {
mappedFields = propertyOperations.computeMappedFieldsForProjection(projection, fields); mappedFields = propertyOperations.computeMappedFieldsForProjection(projection, fields);
if(projection.getMappedType().getType().isInterface()) {
mappedFields = queryMapper.getMappedFields(mappedFields, entity);
}
mappedFields = queryMapper.addMetaAttributes(mappedFields, entity); mappedFields = queryMapper.addMetaAttributes(mappedFields, entity);
} }

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

@ -22,13 +22,13 @@ import java.util.stream.Stream;
import org.bson.Document; import org.bson.Document;
import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.NullUnmarked;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort.Order; import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.AggregationResults;
@ -80,12 +80,7 @@ class AggregationBlocks {
builder.add("\n"); builder.add("\n");
Class<?> outputType = queryMethod.getReturnedObjectType(); Class<?> outputType = getOutputType(queryMethod);
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())) { if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
@ -146,7 +141,6 @@ class AggregationBlocks {
builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName, builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName,
outputType); outputType);
} else { } else {
builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef,
aggregationVariableName, outputType); aggregationVariableName, outputType);
} }
@ -155,6 +149,17 @@ class AggregationBlocks {
return builder.build(); return builder.build();
} }
}
private static Class<?> getOutputType(MongoQueryMethod queryMethod) {
Class<?> outputType = queryMethod.getReturnedObjectType();
if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) {
outputType = Document.class;
} else if (ClassUtils.isAssignable(AggregationResults.class, outputType) && queryMethod.getReturnType().getComponentType() != null) {
outputType = queryMethod.getReturnType().getComponentType().getType();
}
return outputType;
} }
@NullUnmarked @NullUnmarked
@ -173,13 +178,7 @@ class AggregationBlocks {
this.context = context; this.context = context;
this.queryMethod = queryMethod; this.queryMethod = queryMethod;
String parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", "); this.parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", ");
if (StringUtils.hasText(parameterNames)) {
this.parameterNames = ", " + parameterNames;
} else {
this.parameterNames = "";
}
} }
AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) { AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) {
@ -231,7 +230,8 @@ class AggregationBlocks {
builder.add(aggregationStages(context.localVariable("stages"), source.stages())); builder.add(aggregationStages(context.localVariable("stages"), source.stages()));
if (StringUtils.hasText(sortParameter)) { if (StringUtils.hasText(sortParameter)) {
builder.add(sortingStage(sortParameter)); Class<?> outputType = getOutputType(queryMethod);
builder.add(sortingStage(sortParameter, outputType));
} }
if (StringUtils.hasText(limitParameter)) { if (StringUtils.hasText(limitParameter)) {
@ -244,6 +244,7 @@ class AggregationBlocks {
builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName,
context.localVariable("stages")); context.localVariable("stages"));
return builder.build(); return builder.build();
} }
@ -312,7 +313,7 @@ class AggregationBlocks {
return builder.build(); return builder.build();
} }
private CodeBlock sortingStage(String sortProvider) { private CodeBlock sortingStage(String sortProvider, Class<?> outputType) {
Builder builder = CodeBlock.builder(); Builder builder = CodeBlock.builder();
@ -322,8 +323,17 @@ class AggregationBlocks {
builder.addStatement("$1L.append($2L.getProperty(), $2L.isAscending() ? 1 : -1);", builder.addStatement("$1L.append($2L.getProperty(), $2L.isAscending() ? 1 : -1);",
context.localVariable("sortDocument"), context.localVariable("order")); context.localVariable("sortDocument"), context.localVariable("order"));
builder.endControlFlow(); builder.endControlFlow();
builder.addStatement("stages.add(new $T($S, $L))", Document.class, "$sort",
context.localVariable("sortDocument")); if (outputType == Document.class || MongoSimpleTypes.HOLDER.isSimpleType(outputType)
|| ClassUtils.isAssignable(context.getRepositoryInformation().getDomainType(), outputType)) {
builder.addStatement("$L.add(new $T($S, $L))", context.localVariable("stages"), Document.class, "$sort",
context.localVariable("sortDocument"));
} else {
builder.addStatement("$L.add(($T) _ctx -> new $T($S, _ctx.getMappedObject($L, $T.class)))",
context.localVariable("stages"), AggregationOperation.class, Document.class, "$sort",
context.localVariable("sortDocument"), outputType);
}
builder.endControlFlow(); builder.endControlFlow();
return builder.build(); return builder.build();
@ -333,7 +343,7 @@ class AggregationBlocks {
Builder builder = CodeBlock.builder(); Builder builder = CodeBlock.builder();
builder.add(sortingStage(pageableProvider + ".getSort()")); builder.add(sortingStage(pageableProvider + ".getSort()", getOutputType(queryMethod)));
builder.beginControlFlow("if ($L.isPaged())", pageableProvider); builder.beginControlFlow("if ($L.isPaged())", pageableProvider);
builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider); builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider);

55
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java

@ -17,6 +17,7 @@ package org.springframework.data.mongodb.repository.aot;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Box; import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle; import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance; import org.springframework.data.geo.Distance;
@ -52,7 +53,7 @@ class AotPlaceholders {
* @param type * @param type
* @return * @return
*/ */
public static Shape geoJson(int index, String type) { static Shape geoJson(int index, String type) {
return new GeoJsonPlaceholder(index, type); return new GeoJsonPlaceholder(index, type);
} }
@ -62,7 +63,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter. * @param index zero-based index referring to the bindable method parameter.
* @return * @return
*/ */
public static Point point(int index) { static Point point(int index) {
return new PointPlaceholder(index); return new PointPlaceholder(index);
} }
@ -72,7 +73,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter. * @param index zero-based index referring to the bindable method parameter.
* @return * @return
*/ */
public static Shape circle(int index) { static Shape circle(int index) {
return new CirclePlaceholder(index); return new CirclePlaceholder(index);
} }
@ -82,7 +83,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter. * @param index zero-based index referring to the bindable method parameter.
* @return * @return
*/ */
public static Shape box(int index) { static Shape box(int index) {
return new BoxPlaceholder(index); return new BoxPlaceholder(index);
} }
@ -92,7 +93,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter. * @param index zero-based index referring to the bindable method parameter.
* @return * @return
*/ */
public static Shape sphere(int index) { static Shape sphere(int index) {
return new SpherePlaceholder(index); return new SpherePlaceholder(index);
} }
@ -102,20 +103,23 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter. * @param index zero-based index referring to the bindable method parameter.
* @return * @return
*/ */
public static Shape polygon(int index) { static Shape polygon(int index) {
return new PolygonPlaceholder(index); return new PolygonPlaceholder(index);
} }
static RegexPlaceholder regex(int index, @Nullable String options) {
return new RegexPlaceholder(index, options);
}
/** /**
* A placeholder expression used when rending queries to JSON. * A placeholder expression used when rending queries to JSON.
* *
* @since 5.0 * @since 5.0
* @author Christoph Strobl * @author Christoph Strobl
*/ */
public interface Placeholder { interface Placeholder {
String getValue(); String getValue();
} }
/** /**
@ -139,7 +143,7 @@ class AotPlaceholders {
private final int index; private final int index;
public PointPlaceholder(int index) { PointPlaceholder(int index) {
super(Double.NaN, Double.NaN); super(Double.NaN, Double.NaN);
this.index = index; this.index = index;
} }
@ -184,7 +188,7 @@ class AotPlaceholders {
private final int index; private final int index;
public CirclePlaceholder(int index) { CirclePlaceholder(int index) {
super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); // super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); //
this.index = index; this.index = index;
} }
@ -205,7 +209,7 @@ class AotPlaceholders {
private final int index; private final int index;
public BoxPlaceholder(int index) { BoxPlaceholder(int index) {
super(new PointPlaceholder(index), new PointPlaceholder(index)); super(new PointPlaceholder(index), new PointPlaceholder(index));
this.index = index; this.index = index;
} }
@ -226,7 +230,7 @@ class AotPlaceholders {
private final int index; private final int index;
public SpherePlaceholder(int index) { SpherePlaceholder(int index) {
super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); // super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); //
this.index = index; this.index = index;
} }
@ -247,7 +251,7 @@ class AotPlaceholders {
private final int index; private final int index;
public PolygonPlaceholder(int index) { PolygonPlaceholder(int index) {
super(new PointPlaceholder(index), new PointPlaceholder(index), new PointPlaceholder(index), super(new PointPlaceholder(index), new PointPlaceholder(index), new PointPlaceholder(index),
new PointPlaceholder(index)); new PointPlaceholder(index));
this.index = index; this.index = index;
@ -265,4 +269,29 @@ class AotPlaceholders {
} }
static class RegexPlaceholder implements Placeholder {
private final int index;
private final @Nullable String options;
RegexPlaceholder(int index, @Nullable String options) {
this.index = index;
this.options = options;
}
@Nullable String regexOptions() {
return options;
}
@Override
public String getValue() {
return "?" + index;
}
@Override
public String toString() {
return getValue();
}
}
} }

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

@ -19,11 +19,11 @@ import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range; import org.springframework.data.domain.Range;
import org.springframework.data.domain.Score; import org.springframework.data.domain.Score;
@ -47,6 +47,7 @@ import org.springframework.data.mongodb.core.query.TextCriteria;
import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.repository.VectorSearch; import org.springframework.data.mongodb.repository.VectorSearch;
import org.springframework.data.mongodb.repository.aot.AotPlaceholders.Placeholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.Placeholder;
import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder;
import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor; import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoParameterAccessor; import org.springframework.data.mongodb.repository.query.MongoParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoQueryCreator; import org.springframework.data.mongodb.repository.query.MongoQueryCreator;
@ -55,6 +56,8 @@ import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -81,14 +84,14 @@ class AotQueryCreator {
? mqm.isSearchQuery() || source.isAnnotationPresent(VectorSearch.class) ? mqm.isSearchQuery() || source.isAnnotationPresent(VectorSearch.class)
: source.isAnnotationPresent(VectorSearch.class); : source.isAnnotationPresent(VectorSearch.class);
Query query = new AotMongoQueryCreator(partTree, PlaceholderParameterAccessor placeholderAccessor = new PlaceholderParameterAccessor(partTree, queryMethod);
new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(queryMethod)), mappingContext, Query query = new AotMongoQueryCreator(partTree, new PlaceholderConvertingParameterAccessor(placeholderAccessor),
geoNear, searchQuery).createQuery(); mappingContext, geoNear, searchQuery).createQuery();
if (partTree.isLimiting()) { if (partTree.isLimiting()) {
query.limit(partTree.getMaxResults()); query.limit(partTree.getMaxResults());
} }
return new AotStringQuery(query); return new AotStringQuery(query, placeholderAccessor.getPlaceholders());
} }
static class AotMongoQueryCreator extends MongoQueryCreator { static class AotMongoQueryCreator extends MongoQueryCreator {
@ -117,6 +120,25 @@ class AotQueryCreator {
protected Criteria exists(Criteria criteria, Object param) { protected Criteria exists(Criteria criteria, Object param) {
return param instanceof Placeholder p ? criteria.raw("$exists", p) : super.exists(criteria, param); return param instanceof Placeholder p ? criteria.raw("$exists", p) : super.exists(criteria, param);
} }
@Override
protected Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria,
Object param) {
if (part.getType().equals(Type.LIKE)) {
return criteria.is(param);
}
if(part.getType().equals(Type.NOT_LIKE)) {
return criteria.raw("$not", param);
}
if (param instanceof RegexPlaceholder) {
return criteria.raw("$regex", param);
}
return super.createContainingCriteria(part, property, criteria, param);
}
} }
static class PlaceholderConvertingParameterAccessor extends ConvertingParameterAccessor { static class PlaceholderConvertingParameterAccessor extends ConvertingParameterAccessor {
@ -157,12 +179,34 @@ class AotQueryCreator {
private final List<Object> placeholders; private final List<Object> placeholders;
public PlaceholderParameterAccessor(QueryMethod queryMethod) { @Nullable Part getPartForIndex(PartTree partTree, Parameter parameter) {
if(!parameter.isBindable()) {
return null;
}
List<Part> parts = partTree.getParts().stream().toList();
int counter = 0;
for (Part part : parts) {
if(counter == parameter.getIndex()) {
return part;
}
counter += part.getNumberOfArguments();
}
return null;
}
public PlaceholderParameterAccessor(PartTree partTree, QueryMethod queryMethod) {
if (queryMethod.getParameters().getNumberOfParameters() == 0) { if (queryMethod.getParameters().getNumberOfParameters() == 0) {
placeholders = List.of(); placeholders = List.of();
} else { } else {
placeholders = new ArrayList<>(); placeholders = new ArrayList<>();
Parameters<?, ?> parameters = queryMethod.getParameters(); Parameters<?, ?> parameters = queryMethod.getParameters();
for (Parameter parameter : parameters.toList()) { for (Parameter parameter : parameters.toList()) {
if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) { if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), AotPlaceholders.geoJson(parameter.getIndex(), "")); placeholders.add(parameter.getIndex(), AotPlaceholders.geoJson(parameter.getIndex(), ""));
@ -176,8 +220,15 @@ class AotQueryCreator {
placeholders.add(parameter.getIndex(), AotPlaceholders.sphere(parameter.getIndex())); placeholders.add(parameter.getIndex(), AotPlaceholders.sphere(parameter.getIndex()));
} else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) { } else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), AotPlaceholders.polygon(parameter.getIndex())); placeholders.add(parameter.getIndex(), AotPlaceholders.polygon(parameter.getIndex()));
} else if (ClassUtils.isAssignable(Pattern.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), AotPlaceholders.regex(parameter.getIndex(), null));
} else { } else {
placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); Part partForIndex = getPartForIndex(partTree, parameter);
if(partForIndex != null && (partForIndex.getType().equals(Type.LIKE) || partForIndex.getType().equals(Type.NOT_LIKE))) {
placeholders.add(parameter.getIndex(), AotPlaceholders.regex(parameter.getIndex(), partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS) || partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.WHEN_POSSIBLE) ? "i": null));
} else {
placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex()));
}
} }
} }
} }
@ -264,6 +315,10 @@ class AotQueryCreator {
public Iterator<Object> iterator() { public Iterator<Object> iterator() {
return ((List) placeholders).iterator(); return ((List) placeholders).iterator();
} }
public List<Object> getPlaceholders() {
return placeholders;
}
} }
} }

25
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java

@ -15,6 +15,8 @@
*/ */
package org.springframework.data.mongodb.repository.aot; package org.springframework.data.mongodb.repository.aot;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -26,6 +28,8 @@ import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Field; import org.springframework.data.mongodb.core.query.Field;
import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.aot.AotPlaceholders.Placeholder;
import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.mongodb.ReadConcern; import com.mongodb.ReadConcern;
@ -44,8 +48,11 @@ class AotStringQuery extends Query {
private @Nullable String sort; private @Nullable String sort;
private @Nullable String fields; private @Nullable String fields;
public AotStringQuery(Query query) { private List<Object> placeholders = new ArrayList<>();
public AotStringQuery(Query query, List<Object> placeholders) {
this.delegate = query; this.delegate = query;
this.placeholders = placeholders;
} }
public AotStringQuery(String query) { public AotStringQuery(String query) {
@ -72,6 +79,22 @@ class AotStringQuery extends Query {
return this; return this;
} }
boolean isRegexPlaceholderAt(int index) {
if(this.placeholders.isEmpty()) {
return false;
}
return this.placeholders.get(index) instanceof RegexPlaceholder;
}
@Nullable String getRegexOptions(int index) {
if(this.placeholders.isEmpty()) {
return null;
}
return this.placeholders.get(index) instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null;
}
@Override @Override
public Field fields() { public Field fields() {
return delegate.fields(); return delegate.fields();

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

@ -17,14 +17,16 @@ package org.springframework.data.mongodb.repository.aot;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.regex.Pattern;
import org.bson.BsonRegularExpression;
import org.bson.Document; import org.bson.Document;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Range; import org.springframework.data.domain.Range;
import org.springframework.data.domain.Score; import org.springframework.data.domain.Score;
import org.springframework.data.domain.ScoringFunction; import org.springframework.data.domain.ScoringFunction;
@ -45,6 +47,8 @@ import org.springframework.data.mongodb.core.mapping.FieldName;
import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.MongoRegexCreator;
import org.springframework.data.mongodb.core.query.MongoRegexCreator.MatchMode;
import org.springframework.data.mongodb.repository.query.MongoParameters; import org.springframework.data.mongodb.repository.query.MongoParameters;
import org.springframework.data.mongodb.repository.query.MongoParametersParameterAccessor; import org.springframework.data.mongodb.repository.query.MongoParametersParameterAccessor;
import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingContext;
@ -56,6 +60,7 @@ import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.Lazy; import org.springframework.data.util.Lazy;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@ -116,12 +121,12 @@ public class MongoAotRepositoryFragmentSupport {
ParameterBindingContext bindingContext = new ParameterBindingContext(parametersParameterAccessor::getBindableValue, ParameterBindingContext bindingContext = new ParameterBindingContext(parametersParameterAccessor::getBindableValue,
new ValueExpressionEvaluator() { new ValueExpressionEvaluator() {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> @Nullable T evaluate(String expression) { public <T> @Nullable T evaluate(String expression) {
return (T) MongoAotRepositoryFragmentSupport.this.evaluate(method, expression, args); return (T) MongoAotRepositoryFragmentSupport.this.evaluate(method, expression, args);
} }
}); });
return CODEC.decode(source, bindingContext); return CODEC.decode(source, bindingContext);
} }
@ -224,33 +229,72 @@ public class MongoAotRepositoryFragmentSupport {
"Unsupported collation source [%s]".formatted(ObjectUtils.nullSafeClassName(source))); "Unsupported collation source [%s]".formatted(ObjectUtils.nullSafeClassName(source)));
} }
protected Object toRegex(Object source) {
return toRegex(source, null);
}
protected Object toRegex(Object source, @Nullable String options) {
if (source instanceof String sv) {
return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, MatchMode.LIKE), options);
}
if (source instanceof Pattern pattern) {
return pattern;
}
if (source instanceof Collection<?> collection) {
return collection.stream().map(it -> toRegex(it, options)).toList();
}
if (ObjectUtils.isArray(source)) {
return toRegex(List.of(source), options);
}
return source;
}
protected BasicQuery createQuery(Method method, String queryString, Object... parameters) { protected BasicQuery createQuery(Method method, String queryString, Object... parameters) {
Document queryDocument = bindParameters(method, queryString, parameters); Document queryDocument = bindParameters(method, queryString, parameters);
return new BasicQuery(queryDocument); return new BasicQuery(queryDocument);
} }
@SuppressWarnings("NullAway")
protected AggregationPipeline createPipeline(List<Object> rawStages) { protected AggregationPipeline createPipeline(List<Object> rawStages) {
List<AggregationOperation> stages = new ArrayList<>(rawStages.size()); if (rawStages.isEmpty()) {
boolean first = true; return new AggregationPipeline(List.of());
for (Object rawStage : rawStages) { }
if (rawStage instanceof Document stageDocument) {
if (first) { int size = rawStages.size();
stages.add((ctx) -> ctx.getMappedObject(stageDocument)); List<AggregationOperation> stages = new ArrayList<>(size);
} else {
stages.add((ctx) -> stageDocument); Object firstElement = CollectionUtils.firstElement(rawStages);
} stages.add(rawToAggregationOperation(firstElement, true));
} else if (rawStage instanceof AggregationOperation aggregationOperation) {
stages.add(aggregationOperation); if (size == 1) {
return new AggregationPipeline(stages);
}
for (int i = 1; i < size; i++) {
stages.add(rawToAggregationOperation(rawStages.get(i), false));
}
return new AggregationPipeline(stages);
}
private static AggregationOperation rawToAggregationOperation(Object rawStage, boolean requiresMapping) {
if (rawStage instanceof Document stageDocument) {
if (requiresMapping) {
return (ctx) -> ctx.getMappedObject(stageDocument);
} else { } else {
throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass())); return (ctx) -> stageDocument;
}
if (first) {
first = false;
} }
} }
return new AggregationPipeline(stages);
if (rawStage instanceof AggregationOperation aggregationOperation) {
return aggregationOperation;
}
throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass()));
} }
protected List<Object> convertSimpleRawResults(Class<?> targetType, List<Document> rawResults) { protected List<Object> convertSimpleRawResults(Class<?> targetType, List<Document> rawResults) {

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

@ -19,7 +19,6 @@ import java.util.regex.Pattern;
import org.bson.Document; import org.bson.Document;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.mongodb.repository.ReadPreference; import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.aot.AggregationBlocks.AggregationCodeBlockBuilder; import org.springframework.data.mongodb.repository.aot.AggregationBlocks.AggregationCodeBlockBuilder;
@ -164,20 +163,27 @@ class MongoCodeBlocks {
} }
static CodeBlock asDocument(String source, String argNames) { static CodeBlock asDocument(String source, String argNames) {
return asDocument(source, CodeBlock.of("$L", argNames));
}
static CodeBlock asDocument(String source, CodeBlock arguments) {
Builder builder = CodeBlock.builder(); Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) { if (!StringUtils.hasText(source)) {
builder.add("new $T()", Document.class); builder.add("new $T()", Document.class);
} else if (containsPlaceholder(source)) { } else if (containsPlaceholder(source)) {
builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S$L);\n", source, argNames); if (arguments.isEmpty()) {
builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S)", source);
} else {
builder.add("bindParameters(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", source, arguments);
}
} else { } else {
builder.add("parse($S)", source); builder.add("parse($S)", source);
} }
return builder.build(); return builder.build();
} }
static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, String argNames) {
String argNames) {
Builder builder = CodeBlock.builder(); Builder builder = CodeBlock.builder();
if (!StringUtils.hasText(source)) { if (!StringUtils.hasText(source)) {

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

@ -93,6 +93,7 @@ public class MongoRepositoryContributor extends RepositoryContributor {
this.queryCreator = new AotQueryCreator(this.mappingContext); this.queryCreator = new AotQueryCreator(this.mappingContext);
} }
@SuppressWarnings("NullAway")
private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource configSource, ClassLoader classLoader) { private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource configSource, ClassLoader classLoader) {
String location = configSource != null ? configSource.getNamedQueryLocation().orElse(null) : null; String location = configSource != null ? configSource.getNamedQueryLocation().orElse(null) : null;
@ -237,8 +238,12 @@ public class MongoRepositoryContributor extends RepositoryContributor {
} else { } else {
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
query = new QueryInteraction(queryCreator.createQuery(partTree, queryMethod, source), AotStringQuery aotStringQuery = queryCreator.createQuery(partTree, queryMethod, source);
query = new QueryInteraction(aotStringQuery,
partTree.isCountProjection(), partTree.isDelete(), partTree.isExistsProjection()); partTree.isCountProjection(), partTree.isDelete(), partTree.isExistsProjection());
// if(partTree.isLimiting()) {
// query.s
// }
} }
if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) {

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

@ -15,12 +15,13 @@
*/ */
package org.springframework.data.mongodb.repository.aot; package org.springframework.data.mongodb.repository.aot;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.bson.Document; import org.bson.Document;
import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.NullUnmarked;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery;
import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.annotation.Collation; import org.springframework.data.mongodb.core.annotation.Collation;
@ -31,6 +32,7 @@ import org.springframework.data.mongodb.repository.query.MongoQueryExecution.Pag
import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution;
import org.springframework.data.mongodb.repository.query.MongoQueryMethod; import org.springframework.data.mongodb.repository.query.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.util.Lazy;
import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
@ -100,10 +102,17 @@ class QueryBlocks {
} else if (queryMethod.isStreamQuery()) { } else if (queryMethod.isStreamQuery()) {
terminatingMethod = "stream()"; terminatingMethod = "stream()";
} else { } else {
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()"; if (query.getQuery().isLimited()) {
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "first()"
: "firstValue()";
} else {
terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()"
: "oneValue()";
}
} }
if (queryMethod.isPageQuery()) { if (queryMethod.isPageQuery()) {
builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"), builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"),
context.getPageableParameterName(), query.name()); context.getPageableParameterName(), query.name());
} else if (queryMethod.isSliceQuery()) { } else if (queryMethod.isSliceQuery()) {
@ -113,8 +122,20 @@ class QueryBlocks {
String scrollPositionParameterName = context.getScrollPositionParameterName(); String scrollPositionParameterName = context.getScrollPositionParameterName();
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(), if (scrollPositionParameterName != null) {
scrollPositionParameterName);
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(),
scrollPositionParameterName);
} else {
String pageableParameterName = context.getPageableParameterName();
if (pageableParameterName != null) {
builder.addStatement("return $L.matching($L).scroll($L.toScrollPosition())",
context.localVariable("finder"), query.name(), pageableParameterName);
} else {
builder.addStatement("return $L.matching($L).scroll($T.initial())", context.localVariable("finder"),
query.name(), ScrollPosition.class);
}
}
} else { } else {
if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) { if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) {
@ -137,7 +158,7 @@ class QueryBlocks {
private final AotQueryMethodGenerationContext context; private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod; private final MongoQueryMethod queryMethod;
private final String parameterNames; private final Lazy<CodeBlock> queryParameters;
private QueryInteraction source; private QueryInteraction source;
private String queryVariableName; private String queryVariableName;
@ -146,14 +167,54 @@ class QueryBlocks {
this.context = context; this.context = context;
this.queryMethod = queryMethod; this.queryMethod = queryMethod;
this.queryParameters = Lazy.of(this::queryParametersCodeBlock);
}
String parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", "); CodeBlock queryParametersCodeBlock() {
if (StringUtils.hasText(parameterNames)) { List<String> allParameterNames = context.getAllParameterNames();
this.parameterNames = ", " + parameterNames;
} else { if (allParameterNames.isEmpty()) {
this.parameterNames = ""; return CodeBlock.builder().build();
} }
CodeBlock.Builder formatted = CodeBlock.builder();
boolean containsArrayParameter = false;
for (int i = 0; i < allParameterNames.size(); i++) {
String parameterName = allParameterNames.get(i);
Class<?> parameterType = context.getMethodParameter(parameterName).getParameterType();
if (source.getQuery().isRegexPlaceholderAt(i) && parameterType == String.class) {
String regexOptions = source.getQuery().getRegexOptions(i);
if (StringUtils.hasText(regexOptions)) {
formatted.add(CodeBlock.of("toRegex($L)", parameterName));
} else {
formatted.add(CodeBlock.of("toRegex($L, $S)", parameterName, regexOptions));
}
} else {
formatted.add(CodeBlock.of("$L", parameterName));
}
if (i + 1 < allParameterNames.size()) {
formatted.add(", ");
}
if (!containsArrayParameter && parameterType != null && parameterType.isArray()) {
containsArrayParameter = true;
}
}
// wrap single array argument to avoid problems with vargs when calling method
if (containsArrayParameter && allParameterNames.size() == 1) {
return CodeBlock.of("new $T[] { $L }", Object.class, formatted.build());
}
return formatted.build();
}
public CodeBlock getQueryParameters() {
return queryParameters.get();
} }
QueryCodeBlockBuilder filter(QueryInteraction query) { QueryCodeBlockBuilder filter(QueryInteraction query) {
@ -177,24 +238,26 @@ class QueryBlocks {
if (StringUtils.hasText(source.getQuery().getFieldsString())) { if (StringUtils.hasText(source.getQuery().getFieldsString())) {
VariableSnippet fields = Snippet.declare(builder).variable(Document.class, context.localVariable("fields")) VariableSnippet fields = Snippet.declare(builder).variable(Document.class, context.localVariable("fields"))
.of(MongoCodeBlocks.asDocument(source.getQuery().getFieldsString(), parameterNames)); .of(MongoCodeBlocks.asDocument(source.getQuery().getFieldsString(), queryParameters.get()));
builder.addStatement("$L.setFieldsObject($L)", queryVariableName, fields.getVariableName()); builder.addStatement("$L.setFieldsObject($L)", queryVariableName, fields.getVariableName());
} }
String sortParameter = context.getSortParameterName(); if (StringUtils.hasText(source.getQuery().getSortString())) {
if (StringUtils.hasText(sortParameter)) {
builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
} else if (StringUtils.hasText(source.getQuery().getSortString())) {
VariableSnippet sort = Snippet.declare(builder).variable(Document.class, context.localVariable("sort")) VariableSnippet sort = Snippet.declare(builder).variable(Document.class, context.localVariable("sort"))
.of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), parameterNames)); .of(MongoCodeBlocks.asDocument(source.getQuery().getSortString(), getQueryParameters()));
builder.addStatement("$L.setSortObject($L)", queryVariableName, sort.getVariableName()); builder.addStatement("$L.setSortObject($L)", queryVariableName, sort.getVariableName());
} }
String sortParameter = context.getSortParameterName();
if (StringUtils.hasText(sortParameter)) {
builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
}
String limitParameter = context.getLimitParameterName(); String limitParameter = context.getLimitParameterName();
if (StringUtils.hasText(limitParameter)) { if (StringUtils.hasText(limitParameter)) {
builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); builder.addStatement("$L.limit($L)", queryVariableName, limitParameter);
} else if (context.getPageableParameterName() == null && source.getQuery().isLimited()) { } else if (source.getQuery().isLimited()) {
builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit()); builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit());
} }
@ -241,9 +304,15 @@ class QueryBlocks {
org.springframework.data.mongodb.core.query.Collation.class, collationString); org.springframework.data.mongodb.core.query.Collation.class, collationString);
} else { } else {
builder.addStatement( if (getQueryParameters().isEmpty()) {
"$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S$L)))", builder.addStatement(
queryVariableName, collationString, parameterNames); "$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S)))",
queryVariableName, collationString);
} else {
builder.addStatement(
"$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S, $L)))",
queryVariableName, collationString, getQueryParameters());
}
} }
} }
} }
@ -267,10 +336,13 @@ class QueryBlocks {
return CodeBlock.of("new $T(new $T())", BasicQuery.class, Document.class); return CodeBlock.of("new $T(new $T())", BasicQuery.class, Document.class);
} else if (MongoCodeBlocks.containsPlaceholder(source)) { } else if (MongoCodeBlocks.containsPlaceholder(source)) {
Builder builder = CodeBlock.builder(); Builder builder = CodeBlock.builder();
builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S$L)", source, parameterNames); if (getQueryParameters().isEmpty()) {
builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S)", source);
} else {
builder.add("createQuery(ExpressionMarker.class.getEnclosingMethod(), $S, $L)", source, getQueryParameters());
}
return builder.build(); return builder.build();
} } else {
else {
return CodeBlock.of("new $T(parse($S))", BasicQuery.class, source); return CodeBlock.of("new $T(parse($S))", BasicQuery.class, source);
} }
} }

6
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java

@ -23,6 +23,7 @@ import org.springframework.data.mongodb.repository.aot.Snippet.BuilderStyleBuild
import org.springframework.data.mongodb.repository.aot.Snippet.BuilderStyleVariableBuilder.BuilderStyleVariableBuilderImpl; import org.springframework.data.mongodb.repository.aot.Snippet.BuilderStyleVariableBuilder.BuilderStyleVariableBuilderImpl;
import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.util.Assert;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
@ -192,12 +193,17 @@ interface Snippet {
@Override @Override
public BuilderStyleBuilder with(Snippet snippet) { public BuilderStyleBuilder with(Snippet snippet) {
Assert.notNull(targetMethodName, "TargetMethodName must be set before calling this method");
new BuilderStyleSnippet(targetVariableName, targetMethodName, snippet).appendTo(targetBuilder); new BuilderStyleSnippet(targetVariableName, targetMethodName, snippet).appendTo(targetBuilder);
return this; return this;
} }
@Override @Override
public VariableSnippet variable() { public VariableSnippet variable() {
Assert.notNull(variableSnippet, "VariableSnippet must not be null");
return this.variableSnippet; return this.variableSnippet;
} }
} }

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

@ -205,11 +205,11 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
case STARTING_WITH: case STARTING_WITH:
case ENDING_WITH: case ENDING_WITH:
case CONTAINING: case CONTAINING:
return createContainingCriteria(part, property, criteria, parameters); return createContainingCriteria(part, property, criteria, parameters.next());
case NOT_LIKE: case NOT_LIKE:
return createContainingCriteria(part, property, criteria.not(), parameters); return createContainingCriteria(part, property, criteria.not(), parameters.next());
case NOT_CONTAINING: case NOT_CONTAINING:
return createContainingCriteria(part, property, criteria.not(), parameters); return createContainingCriteria(part, property, criteria.not(), parameters.next());
case REGEX: case REGEX:
return regex(criteria, parameters.next()); return regex(criteria, parameters.next());
case EXISTS: case EXISTS:
@ -258,8 +258,9 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
Iterator<Object> parameters) { Iterator<Object> parameters) {
Range<Distance> range = accessor.getDistanceRange(); Range<Distance> range = accessor.getDistanceRange();
Optional<Distance> distance = range.getUpperBound().getValue();
Optional<Distance> minDistance = range.getLowerBound().getValue(); Optional<Distance> distance = range != null ? range.getUpperBound().getValue() : Optional.empty();
Optional<Distance> minDistance = range != null ? range.getLowerBound().getValue() : Optional.empty();
Point point = accessor.getGeoNearLocation(); Point point = accessor.getGeoNearLocation();
Point pointToUse = point == null ? nextAs(parameters, Point.class) : point; Point pointToUse = point == null ? nextAs(parameters, Point.class) : point;
@ -344,18 +345,17 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
* @param part * @param part
* @param property * @param property
* @param criteria * @param criteria
* @param parameters * @param parameter
* @return * @return
*/ */
private Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria, protected Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria,
Iterator<Object> parameters) { Object parameter) {
if (property.isCollectionLike()) { if (property.isCollectionLike()) {
Object next = parameters.next(); return in(criteria, part, parameter);
return in(criteria, part, next);
} }
return addAppropriateLikeRegexTo(criteria, part, parameters.next()); return addAppropriateLikeRegexTo(criteria, part, parameter);
} }
/** /**
@ -375,6 +375,10 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
"Argument for creating $regex pattern for property '%s' must not be null", part.getProperty().getSegment())); "Argument for creating $regex pattern for property '%s' must not be null", part.getProperty().getSegment()));
} }
if(value instanceof Pattern pattern) {
return criteria.regex(pattern);
}
return criteria.regex(toLikeRegex(value.toString(), part), toRegexOptions(part)); return criteria.regex(toLikeRegex(value.toString(), part), toRegexOptions(part));
} }
@ -426,7 +430,6 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
streamable = streamable.map(it -> { streamable = streamable.map(it -> {
if (it instanceof String sv) { if (it instanceof String sv) {
return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions); return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions);
} }
return it; return it;

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

@ -202,7 +202,7 @@ public interface MongoQueryExecution {
} }
Range<Distance> distances = accessor.getDistanceRange(); Range<Distance> distances = accessor.getDistanceRange();
Assert.notNull(nearLocation, "[query.distance] must not be null"); Assert.notNull(distances, "[query.distances] must not be null");
distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric())); distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric()));
distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric())); distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric()));

85
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java

@ -15,10 +15,31 @@
*/ */
package org.springframework.data.mongodb.core.aggregation; package org.springframework.data.mongodb.core.aggregation;
import static org.springframework.data.domain.Sort.Direction.*; import static org.springframework.data.domain.Sort.Direction.ASC;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.domain.Sort.Direction.DESC;
import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.DEFAULT_CONTEXT;
import static org.springframework.data.mongodb.test.util.Assertions.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.bind;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.bucket;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.bucketAuto;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.count;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.facet;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.group;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.limit;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.lookup;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.match;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregationOptions;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.out;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.previousOperation;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.project;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.replaceRoot;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.sample;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.skip;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.sort;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.unwind;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import static org.springframework.data.mongodb.test.util.Assertions.assertThatIllegalArgumentException;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.text.ParseException; import java.text.ParseException;
@ -46,7 +67,6 @@ import org.bson.Document;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@ -554,7 +574,7 @@ public class AggregationTests {
/* /*
//complex mongodb aggregation framework example from //complex mongodb aggregation framework example from
https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state
db.zipcodes.aggregate( db.zipcodes.aggregate(
{ {
$group: { $group: {
@ -1579,7 +1599,8 @@ public class AggregationTests {
createUsersWithReferencedPersons(); createUsersWithReferencedPersons();
TypedAggregation<User> agg = newAggregation(User.class, // TypedAggregation<User> agg = newAggregation(User.class, //
lookup().from("person").localField("_id").foreignField("firstname").pipeline(match(where("firstname").is("u1"))).as("linkedPerson"), // lookup().from("person").localField("_id").foreignField("firstname").pipeline(match(where("firstname").is("u1")))
.as("linkedPerson"), //
sort(ASC, "id")); sort(ASC, "id"));
AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class); AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class);
@ -1598,8 +1619,10 @@ public class AggregationTests {
createUsersWithReferencedPersons(); createUsersWithReferencedPersons();
TypedAggregation<User> agg = newAggregation(User.class, // TypedAggregation<User> agg = newAggregation(User.class, //
lookup().from("person").localField("_id").foreignField("firstname").let(Let.ExpressionVariable.newVariable("the_id").forField("_id")).pipeline( lookup().from("person").localField("_id").foreignField("firstname")
match(ctx -> new Document("$expr", new Document("$eq", List.of("$$the_id", "u1"))))).as("linkedPerson"), .let(Let.ExpressionVariable.newVariable("the_id").forField("_id"))
.pipeline(match(ctx -> new Document("$expr", new Document("$eq", List.of("$$the_id", "u1")))))
.as("linkedPerson"),
sort(ASC, "id")); sort(ASC, "id"));
AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class); AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class);
@ -1956,9 +1979,8 @@ public class AggregationTests {
mongoTemplate.insert(objectToSave); mongoTemplate.insert(objectToSave);
mongoTemplate.insert(objectToSave2); mongoTemplate.insert(objectToSave2);
Aggregation agg = Aggregation.newAggregation( Aggregation agg = Aggregation.newAggregation(project()
project().and(ArithmeticOperators.valueOf("x").percentile(0.9, 0.4).and("y").and("xField")) .and(ArithmeticOperators.valueOf("x").percentile(0.9, 0.4).and("y").and("xField")).as("percentileValues"));
.as("percentileValues"));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); AggregationResults<Document> result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class);
@ -1979,8 +2001,7 @@ public class AggregationTests {
mongoTemplate.insert(objectToSave2); mongoTemplate.insert(objectToSave2);
Aggregation agg = Aggregation.newAggregation( Aggregation agg = Aggregation.newAggregation(
project().and(ArithmeticOperators.valueOf("x").median().and("y").and("xField")) project().and(ArithmeticOperators.valueOf("x").median().and("y").and("xField")).as("medianValue"));
.as("medianValue"));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); AggregationResults<Document> result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class);
@ -2086,7 +2107,8 @@ public class AggregationTests {
mongoTemplate.save(widget); mongoTemplate.save(widget);
Criteria criteria = Criteria.where("users").elemMatch(Criteria.where("id").is("4ee921aca44fd11b3254e001")); Criteria criteria = Criteria.where("users").elemMatch(Criteria.where("id").is("4ee921aca44fd11b3254e001"));
AggregationResults<Widget> aggregate = mongoTemplate.aggregate(newAggregation(match(criteria)), Widget.class, Widget.class); AggregationResults<Widget> aggregate = mongoTemplate.aggregate(newAggregation(match(criteria)), Widget.class,
Widget.class);
assertThat(aggregate.getMappedResults()).contains(widget); assertThat(aggregate.getMappedResults()).contains(widget);
} }
@ -2097,9 +2119,7 @@ public class AggregationTests {
Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build(); Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build();
mongoTemplate.insert(Arrays.asList(item1, item2), Item.class); mongoTemplate.insert(Arrays.asList(item1, item2), Item.class);
TypedAggregation<Item> aggregation = newAggregation(Item.class, TypedAggregation<Item> aggregation = newAggregation(Item.class, match(where("itemId").is("1")), unwind("tags"),
match(where("itemId").is("1")),
unwind("tags"),
match(where("itemId").is("1").and("tags").is("c"))); match(where("itemId").is("1").and("tags").is("c")));
AggregationResults<Document> results = mongoTemplate.aggregate(aggregation, Document.class); AggregationResults<Document> results = mongoTemplate.aggregate(aggregation, Document.class);
List<Document> mappedResults = results.getMappedResults(); List<Document> mappedResults = results.getMappedResults();
@ -2114,9 +2134,7 @@ public class AggregationTests {
Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build(); Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build();
mongoTemplate.insert(Arrays.asList(item1, item2), Item.class); mongoTemplate.insert(Arrays.asList(item1, item2), Item.class);
TypedAggregation<Item> aggregation = newAggregation(Item.class, TypedAggregation<Item> aggregation = newAggregation(Item.class, match(where("itemId").is("1")), unwind("tags"),
match(where("itemId").is("1")),
unwind("tags"),
project().and("itemId").as("itemId").and("tags").as("tags"), project().and("itemId").as("itemId").and("tags").as("tags"),
match(where("itemId").is("1").and("tags").is("c"))); match(where("itemId").is("1").and("tags").is("c")));
@ -3097,4 +3115,29 @@ public class AggregationTests {
return "AggregationTests.UserRef(id=" + this.getId() + ", name=" + this.getName() + ")"; return "AggregationTests.UserRef(id=" + this.getId() + ", name=" + this.getName() + ")";
} }
} }
@Test
void xxx() {
MyOhMy source = new MyOhMy();
source.id = "id-1";
source.firstname = "iwi";
source.lastname = "wang";
mongoTemplate.save(source);
TypedAggregation<MyOhMy> agg = newAggregation(MyOhMy.class, project("firstname"));
AggregationResults<MyMyOh> aggregate = mongoTemplate.aggregate(agg, MyMyOh.class);
assertThat(aggregate.getMappedResults()).hasOnlyElementsOfType(MyMyOh.class);
}
static class MyOhMy {
@Id String id;
String firstname;
String lastname;
}
interface MyMyOh {
String getFirstname();
}
} }

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

@ -198,10 +198,16 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
assertThat(result).hasSize(1).contains(boyd); assertThat(result).hasSize(1).contains(boyd);
} }
@Test // GH-5027
void findsPersonsByFirstnameLikeWithPattern() {
List<Person> result = repository.findByFirstnameLike(Pattern.compile("Bo.*"));
assertThat(result).hasSize(1).contains(boyd);
}
@Test // DATAMONGO-1608 @Test // DATAMONGO-1608
void findByFirstnameLikeWithNull() { void findByFirstnameLikeWithNull() {
assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameLike((String)null));
assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameLike(null));
} }
@Test @Test
@ -752,7 +758,6 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
@Test // DATAMONGO-1608 @Test // DATAMONGO-1608
void findByFirstNameIgnoreCaseWithNull() { void findByFirstNameIgnoreCaseWithNull() {
assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameIgnoreCase(null)); assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameIgnoreCase(null));
} }
@ -1304,6 +1309,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
assertThat(result).doesNotContain(boyd); assertThat(result).doesNotContain(boyd);
} }
@Test // GH-5027
void findsPersonsByFirstnameNotLikeWithPattern() {
List<Person> result = repository.findByFirstnameNotLike(Pattern.compile("Bo.*"));
assertThat(result).hasSize((int) (repository.count() - 1));
assertThat(result).doesNotContain(boyd);
}
@Test // DATAMONGO-1539 @Test // DATAMONGO-1539
void countsPersonsByFirstname() { void countsPersonsByFirstname() {
assertThat(repository.countByThePersonsFirstname("Dave")).isEqualTo(1L); assertThat(repository.countByThePersonsFirstname("Dave")).isEqualTo(1L);
@ -1662,7 +1675,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
@Test // GH-3656 @Test // GH-3656
@DirtiesState @DirtiesState
void resultProjectionWithOptionalIsExcecutedCorrectly() { void resultProjectionWithOptionalIsExecutedCorrectly() {
carter.setAddress(new Address("batman", "robin", "gotham")); carter.setAddress(new Address("batman", "robin", "gotham"));
repository.save(carter); repository.save(carter);

33
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java

@ -16,7 +16,7 @@
package org.springframework.data.mongodb.repository; package org.springframework.data.mongodb.repository;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -34,7 +34,6 @@ import org.springframework.test.context.ContextConfiguration;
* @author Mark Paluch * @author Mark Paluch
*/ */
@ContextConfiguration(classes = AotPersonRepositoryIntegrationTests.Config.class) @ContextConfiguration(classes = AotPersonRepositoryIntegrationTests.Config.class)
@Disabled("Several mismatches, some class-loader visibility issues and some behavioral differences remain to be fixed")
class AotPersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { class AotPersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests {
@Configuration @Configuration
@ -61,4 +60,34 @@ class AotPersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegr
} }
@Test // DATAMONGO-1608
@Disabled
void findByFirstnameLikeWithNull() {
super.findByFirstnameLikeWithNull();
}
@Test // GH-3395
@Disabled
void caseInSensitiveInClauseQuotesExpressions() {
super.caseInSensitiveInClauseQuotesExpressions();
}
@Test // DATAMONGO-1608
@Disabled
void findByFirstNameIgnoreCaseWithNull() {
super.findByFirstNameIgnoreCaseWithNull();
}
@Test // GH-3395
@Disabled
void caseSensitiveInClauseIgnoresExpressions() {
super.caseSensitiveInClauseIgnoresExpressions();
}
@Test // GH-3395, GH-4404
@Disabled
void caseInSensitiveInClause() {
super.caseInSensitiveInClause();
}
} }

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

@ -28,7 +28,7 @@ import org.springframework.data.annotation.PersistenceCreator;
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
*/ */
final class PersonAggregate { public final class PersonAggregate {
@Id private final String lastname; @Id private final String lastname;
private final Set<String> names; private final Set<String> names;

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

@ -98,6 +98,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return * @return
*/ */
List<Person> findByFirstnameLike(@Nullable String firstname); List<Person> findByFirstnameLike(@Nullable String firstname);
List<Person> findByFirstnameLike(Pattern firstname);
List<Person> findByFirstnameNotContains(String firstname); List<Person> findByFirstnameNotContains(String firstname);
@ -108,8 +109,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return * @return
*/ */
List<Person> findByFirstnameNotLike(String firstname); List<Person> findByFirstnameNotLike(String firstname);
List<Person> findByFirstnameNotLike(Pattern firstname);
List<Person> findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort); List<Person> findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort);
List<Person> findByFirstnameLikeOrderByLastnameAsc(Pattern firstname, Sort sort);
List<Person> findBySkillsContains(List<String> skills); List<Person> findBySkillsContains(List<String> skills);
@ -128,8 +131,13 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
ScrollPosition scrollPosition); ScrollPosition scrollPosition);
Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname,
ScrollPosition scrollPosition);
Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
ScrollPosition scrollPosition, Limit limit); ScrollPosition scrollPosition, Limit limit);
Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname,
ScrollPosition scrollPosition, Limit limit);
/** /**
* Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards * Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards
@ -140,6 +148,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return * @return
*/ */
Window<PersonSummaryDto> findCursorProjectionByLastnameLike(String lastname, Pageable pageable); Window<PersonSummaryDto> findCursorProjectionByLastnameLike(String lastname, Pageable pageable);
Window<PersonSummaryDto> findCursorProjectionByLastnameLike(Pattern lastname, Pageable pageable);
/** /**
* Returns a page of {@link Person}s with a lastname matching the given one (*-wildcards supported). * Returns a page of {@link Person}s with a lastname matching the given one (*-wildcards supported).
@ -149,8 +158,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return * @return
*/ */
Page<Person> findByLastnameLike(String lastname, Pageable pageable); Page<Person> findByLastnameLike(String lastname, Pageable pageable);
Page<Person> findByLastnameLike(Pattern lastname, Pageable pageable);
List<Person> findByLastnameLike(String lastname, Sort sort, Limit limit); List<Person> findByLastnameLike(String lastname, Sort sort, Limit limit);
List<Person> findByLastnameLike(Pattern lastname, Sort sort, Limit limit);
@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}") @Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable); Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);

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

@ -20,7 +20,7 @@ import java.util.Objects;
/** /**
* @author Christoph Strobl * @author Christoph Strobl
*/ */
final class SumAge { public final class SumAge {
private final Long total; private final Long total;

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

@ -27,13 +27,17 @@ import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.test.tools.TestCompiler; import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.Lazy;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
/** /**
@ -124,6 +128,8 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce
return new RepositoryFactoryBeanSupport.FragmentCreationContext() { return new RepositoryFactoryBeanSupport.FragmentCreationContext() {
final Lazy<ProjectionFactory> projectionFactory = Lazy.of(SpelAwareProxyProjectionFactory::new);
@Override @Override
public RepositoryMetadata getRepositoryMetadata() { public RepositoryMetadata getRepositoryMetadata() {
return repositoryContext.getRepositoryInformation(); return repositoryContext.getRepositoryInformation();
@ -131,12 +137,15 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce
@Override @Override
public ValueExpressionDelegate getValueExpressionDelegate() { public ValueExpressionDelegate getValueExpressionDelegate() {
return ValueExpressionDelegate.create();
QueryMethodValueEvaluationContextAccessor queryMethodValueEvaluationContextAccessor = new QueryMethodValueEvaluationContextAccessor(
new StandardEnvironment(), repositoryContext.getBeanFactory());
return new ValueExpressionDelegate(queryMethodValueEvaluationContextAccessor, ValueExpressionParser.create());
} }
@Override @Override
public ProjectionFactory getProjectionFactory() { public ProjectionFactory getProjectionFactory() {
return new SpelAwareProxyProjectionFactory(); return projectionFactory.get();
} }
}; };
} }

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

@ -72,15 +72,25 @@ These are typically all query methods that are not backed by an xref:repositorie
* Methods annotated with `@Aggregation`, `@Update`, and `@VectorSearch` * Methods annotated with `@Aggregation`, `@Update`, and `@VectorSearch`
* `@Hint`, `@Meta`, and `@ReadPreference` support * `@Hint`, `@Meta`, and `@ReadPreference` support
* `Page`, `Slice`, and `Optional` return types * `Page`, `Slice`, and `Optional` return types
* DTO Projections * DTO & Interface Projections
**Limitations** **Limitations**
* `@Meta.allowDiskUse` and `flags` are not evaluated. * `@Meta.allowDiskUse` and `flags` are not evaluated.
* Limited `Collation` detection. * Limited `Collation` detection.
* No support for in-clauses with pattern matching / case insensitivity
**Excluded methods** **Excluded methods**
* `CrudRepository` and other base interface methods * `CrudRepository` and other base interface methods
* Querydsl and Query by Example methods * Querydsl and Query by Example methods
* Query Methods obtaining MQL from a file * Query Methods obtaining MQL from a file
[TIP]
====
Consider using `Pattern` instead of `String` as parameter type when working with derived queries using the `LIKE` keyword.
[source,java]
----
List<Person> findByLastnameLike(Pattern lastname);
----
====

Loading…
Cancel
Save