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. 83
      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; @@ -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.validation.Validator;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.util.CloseableIterator;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -2272,8 +2274,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2272,8 +2274,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
<T, O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<T> outputType,
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));
}
AggregationOptions options = aggregation.getOptions();
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 { @@ -377,6 +377,9 @@ class QueryOperations {
mappedFields = queryMapper.getMappedFields(fields, entity);
} else {
mappedFields = propertyOperations.computeMappedFieldsForProjection(projection, fields);
if(projection.getMappedType().getType().isInterface()) {
mappedFields = queryMapper.getMappedFields(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; @@ -22,13 +22,13 @@ import java.util.stream.Stream;
import org.bson.Document;
import org.jspecify.annotations.NullUnmarked;
import org.springframework.core.ResolvableType;
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.MongoOperations;
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.AggregationPipeline;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
@ -80,12 +80,7 @@ class AggregationBlocks { @@ -80,12 +80,7 @@ class AggregationBlocks {
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();
}
Class<?> outputType = getOutputType(queryMethod);
if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) {
builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType);
@ -146,7 +141,6 @@ class AggregationBlocks { @@ -146,7 +141,6 @@ class AggregationBlocks {
builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName,
outputType);
} else {
builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef,
aggregationVariableName, outputType);
}
@ -155,6 +149,17 @@ class AggregationBlocks { @@ -155,6 +149,17 @@ class AggregationBlocks {
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
@ -173,13 +178,7 @@ class AggregationBlocks { @@ -173,13 +178,7 @@ class AggregationBlocks {
this.context = context;
this.queryMethod = queryMethod;
String parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", ");
if (StringUtils.hasText(parameterNames)) {
this.parameterNames = ", " + parameterNames;
} else {
this.parameterNames = "";
}
this.parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", ");
}
AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) {
@ -231,7 +230,8 @@ class AggregationBlocks { @@ -231,7 +230,8 @@ class AggregationBlocks {
builder.add(aggregationStages(context.localVariable("stages"), source.stages()));
if (StringUtils.hasText(sortParameter)) {
builder.add(sortingStage(sortParameter));
Class<?> outputType = getOutputType(queryMethod);
builder.add(sortingStage(sortParameter, outputType));
}
if (StringUtils.hasText(limitParameter)) {
@ -244,6 +244,7 @@ class AggregationBlocks { @@ -244,6 +244,7 @@ class AggregationBlocks {
builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName,
context.localVariable("stages"));
return builder.build();
}
@ -312,7 +313,7 @@ class AggregationBlocks { @@ -312,7 +313,7 @@ class AggregationBlocks {
return builder.build();
}
private CodeBlock sortingStage(String sortProvider) {
private CodeBlock sortingStage(String sortProvider, Class<?> outputType) {
Builder builder = CodeBlock.builder();
@ -322,8 +323,17 @@ class AggregationBlocks { @@ -322,8 +323,17 @@ class AggregationBlocks {
builder.addStatement("$1L.append($2L.getProperty(), $2L.isAscending() ? 1 : -1);",
context.localVariable("sortDocument"), context.localVariable("order"));
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();
return builder.build();
@ -333,7 +343,7 @@ class AggregationBlocks { @@ -333,7 +343,7 @@ class AggregationBlocks {
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.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; @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.repository.aot;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
@ -52,7 +53,7 @@ class AotPlaceholders { @@ -52,7 +53,7 @@ class AotPlaceholders {
* @param type
* @return
*/
public static Shape geoJson(int index, String type) {
static Shape geoJson(int index, String type) {
return new GeoJsonPlaceholder(index, type);
}
@ -62,7 +63,7 @@ class AotPlaceholders { @@ -62,7 +63,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter.
* @return
*/
public static Point point(int index) {
static Point point(int index) {
return new PointPlaceholder(index);
}
@ -72,7 +73,7 @@ class AotPlaceholders { @@ -72,7 +73,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter.
* @return
*/
public static Shape circle(int index) {
static Shape circle(int index) {
return new CirclePlaceholder(index);
}
@ -82,7 +83,7 @@ class AotPlaceholders { @@ -82,7 +83,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter.
* @return
*/
public static Shape box(int index) {
static Shape box(int index) {
return new BoxPlaceholder(index);
}
@ -92,7 +93,7 @@ class AotPlaceholders { @@ -92,7 +93,7 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter.
* @return
*/
public static Shape sphere(int index) {
static Shape sphere(int index) {
return new SpherePlaceholder(index);
}
@ -102,20 +103,23 @@ class AotPlaceholders { @@ -102,20 +103,23 @@ class AotPlaceholders {
* @param index zero-based index referring to the bindable method parameter.
* @return
*/
public static Shape polygon(int index) {
static Shape polygon(int 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.
*
* @since 5.0
* @author Christoph Strobl
*/
public interface Placeholder {
interface Placeholder {
String getValue();
}
/**
@ -139,7 +143,7 @@ class AotPlaceholders { @@ -139,7 +143,7 @@ class AotPlaceholders {
private final int index;
public PointPlaceholder(int index) {
PointPlaceholder(int index) {
super(Double.NaN, Double.NaN);
this.index = index;
}
@ -184,7 +188,7 @@ class AotPlaceholders { @@ -184,7 +188,7 @@ class AotPlaceholders {
private final int index;
public CirclePlaceholder(int index) {
CirclePlaceholder(int index) {
super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); //
this.index = index;
}
@ -205,7 +209,7 @@ class AotPlaceholders { @@ -205,7 +209,7 @@ class AotPlaceholders {
private final int index;
public BoxPlaceholder(int index) {
BoxPlaceholder(int index) {
super(new PointPlaceholder(index), new PointPlaceholder(index));
this.index = index;
}
@ -226,7 +230,7 @@ class AotPlaceholders { @@ -226,7 +230,7 @@ class AotPlaceholders {
private final int index;
public SpherePlaceholder(int index) {
SpherePlaceholder(int index) {
super(new PointPlaceholder(index), Distance.of(1, Metrics.NEUTRAL)); //
this.index = index;
}
@ -247,7 +251,7 @@ class AotPlaceholders { @@ -247,7 +251,7 @@ class AotPlaceholders {
private final int index;
public PolygonPlaceholder(int index) {
PolygonPlaceholder(int index) {
super(new PointPlaceholder(index), new PointPlaceholder(index), new PointPlaceholder(index),
new PointPlaceholder(index));
this.index = index;
@ -265,4 +269,29 @@ class AotPlaceholders { @@ -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; @@ -19,11 +19,11 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
import org.bson.conversions.Bson;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Score;
@ -47,6 +47,7 @@ import org.springframework.data.mongodb.core.query.TextCriteria; @@ -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.repository.VectorSearch;
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.MongoParameterAccessor;
import org.springframework.data.mongodb.repository.query.MongoQueryCreator;
@ -55,6 +56,8 @@ import org.springframework.data.repository.query.Parameter; @@ -55,6 +56,8 @@ import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethod;
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.util.TypeInformation;
import org.springframework.util.ClassUtils;
@ -81,14 +84,14 @@ class AotQueryCreator { @@ -81,14 +84,14 @@ class AotQueryCreator {
? mqm.isSearchQuery() || source.isAnnotationPresent(VectorSearch.class)
: source.isAnnotationPresent(VectorSearch.class);
Query query = new AotMongoQueryCreator(partTree,
new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(queryMethod)), mappingContext,
geoNear, searchQuery).createQuery();
PlaceholderParameterAccessor placeholderAccessor = new PlaceholderParameterAccessor(partTree, queryMethod);
Query query = new AotMongoQueryCreator(partTree, new PlaceholderConvertingParameterAccessor(placeholderAccessor),
mappingContext, geoNear, searchQuery).createQuery();
if (partTree.isLimiting()) {
query.limit(partTree.getMaxResults());
}
return new AotStringQuery(query);
return new AotStringQuery(query, placeholderAccessor.getPlaceholders());
}
static class AotMongoQueryCreator extends MongoQueryCreator {
@ -117,6 +120,25 @@ class AotQueryCreator { @@ -117,6 +120,25 @@ class AotQueryCreator {
protected Criteria exists(Criteria criteria, Object 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 {
@ -157,12 +179,34 @@ class AotQueryCreator { @@ -157,12 +179,34 @@ class AotQueryCreator {
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) {
placeholders = List.of();
} else {
placeholders = new ArrayList<>();
Parameters<?, ?> parameters = queryMethod.getParameters();
for (Parameter parameter : parameters.toList()) {
if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) {
placeholders.add(parameter.getIndex(), AotPlaceholders.geoJson(parameter.getIndex(), ""));
@ -176,8 +220,15 @@ class AotQueryCreator { @@ -176,8 +220,15 @@ class AotQueryCreator {
placeholders.add(parameter.getIndex(), AotPlaceholders.sphere(parameter.getIndex()));
} else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) {
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 {
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 { @@ -264,6 +315,10 @@ class AotQueryCreator {
public Iterator<Object> 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 @@ @@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@ -26,6 +28,8 @@ import org.springframework.data.mongodb.core.query.Collation; @@ -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.Meta;
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 com.mongodb.ReadConcern;
@ -44,8 +48,11 @@ class AotStringQuery extends Query { @@ -44,8 +48,11 @@ class AotStringQuery extends Query {
private @Nullable String sort;
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.placeholders = placeholders;
}
public AotStringQuery(String query) {
@ -72,6 +79,22 @@ class AotStringQuery extends Query { @@ -72,6 +79,22 @@ class AotStringQuery extends Query {
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
public Field 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; @@ -17,14 +17,16 @@ package org.springframework.data.mongodb.repository.aot;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import org.bson.BsonRegularExpression;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Score;
import org.springframework.data.domain.ScoringFunction;
@ -45,6 +47,8 @@ import org.springframework.data.mongodb.core.mapping.FieldName; @@ -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.Collation;
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.MongoParametersParameterAccessor;
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
@ -56,6 +60,7 @@ import org.springframework.data.repository.query.ParametersSource; @@ -56,6 +60,7 @@ import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.Lazy;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ObjectUtils;
@ -116,12 +121,12 @@ public class MongoAotRepositoryFragmentSupport { @@ -116,12 +121,12 @@ public class MongoAotRepositoryFragmentSupport {
ParameterBindingContext bindingContext = new ParameterBindingContext(parametersParameterAccessor::getBindableValue,
new ValueExpressionEvaluator() {
@Override
@SuppressWarnings("unchecked")
public <T> @Nullable T evaluate(String expression) {
@Override
@SuppressWarnings("unchecked")
public <T> @Nullable T evaluate(String expression) {
return (T) MongoAotRepositoryFragmentSupport.this.evaluate(method, expression, args);
}
});
}
});
return CODEC.decode(source, bindingContext);
}
@ -224,33 +229,72 @@ public class MongoAotRepositoryFragmentSupport { @@ -224,33 +229,72 @@ public class MongoAotRepositoryFragmentSupport {
"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) {
Document queryDocument = bindParameters(method, queryString, parameters);
return new BasicQuery(queryDocument);
}
@SuppressWarnings("NullAway")
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);
if (rawStages.isEmpty()) {
return new AggregationPipeline(List.of());
}
int size = rawStages.size();
List<AggregationOperation> stages = new ArrayList<>(size);
Object firstElement = CollectionUtils.firstElement(rawStages);
stages.add(rawToAggregationOperation(firstElement, true));
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 {
throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass()));
}
if (first) {
first = false;
return (ctx) -> stageDocument;
}
}
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) {

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

@ -19,7 +19,6 @@ import java.util.regex.Pattern; @@ -19,7 +19,6 @@ import java.util.regex.Pattern;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.mongodb.repository.ReadPreference;
import org.springframework.data.mongodb.repository.aot.AggregationBlocks.AggregationCodeBlockBuilder;
@ -164,20 +163,27 @@ class MongoCodeBlocks { @@ -164,20 +163,27 @@ class MongoCodeBlocks {
}
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();
if (!StringUtils.hasText(source)) {
builder.add("new $T()", Document.class);
} 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 {
builder.add("parse($S)", source);
}
return builder.build();
}
static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName,
String argNames) {
static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, String argNames) {
Builder builder = CodeBlock.builder();
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 { @@ -93,6 +93,7 @@ public class MongoRepositoryContributor extends RepositoryContributor {
this.queryCreator = new AotQueryCreator(this.mappingContext);
}
@SuppressWarnings("NullAway")
private NamedQueries getNamedQueries(@Nullable RepositoryConfigurationSource configSource, ClassLoader classLoader) {
String location = configSource != null ? configSource.getNamedQueryLocation().orElse(null) : null;
@ -237,8 +238,12 @@ public class MongoRepositoryContributor extends RepositoryContributor { @@ -237,8 +238,12 @@ public class MongoRepositoryContributor extends RepositoryContributor {
} else {
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());
// if(partTree.isLimiting()) {
// query.s
// }
}
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 @@ @@ -15,12 +15,13 @@
*/
package org.springframework.data.mongodb.repository.aot;
import java.util.List;
import java.util.Optional;
import org.bson.Document;
import org.jspecify.annotations.NullUnmarked;
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.MongoOperations;
import org.springframework.data.mongodb.core.annotation.Collation;
@ -31,6 +32,7 @@ import org.springframework.data.mongodb.repository.query.MongoQueryExecution.Pag @@ -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.MongoQueryMethod;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.util.Lazy;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.TypeName;
@ -100,10 +102,17 @@ class QueryBlocks { @@ -100,10 +102,17 @@ class QueryBlocks {
} else if (queryMethod.isStreamQuery()) {
terminatingMethod = "stream()";
} 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()) {
builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"),
context.getPageableParameterName(), query.name());
} else if (queryMethod.isSliceQuery()) {
@ -113,8 +122,20 @@ class QueryBlocks { @@ -113,8 +122,20 @@ class QueryBlocks {
String scrollPositionParameterName = context.getScrollPositionParameterName();
builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(),
scrollPositionParameterName);
if (scrollPositionParameterName != null) {
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 {
if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) {
@ -137,7 +158,7 @@ class QueryBlocks { @@ -137,7 +158,7 @@ class QueryBlocks {
private final AotQueryMethodGenerationContext context;
private final MongoQueryMethod queryMethod;
private final String parameterNames;
private final Lazy<CodeBlock> queryParameters;
private QueryInteraction source;
private String queryVariableName;
@ -146,14 +167,54 @@ class QueryBlocks { @@ -146,14 +167,54 @@ class QueryBlocks {
this.context = context;
this.queryMethod = queryMethod;
this.queryParameters = Lazy.of(this::queryParametersCodeBlock);
}
String parameterNames = StringUtils.collectionToDelimitedString(context.getAllParameterNames(), ", ");
CodeBlock queryParametersCodeBlock() {
if (StringUtils.hasText(parameterNames)) {
this.parameterNames = ", " + parameterNames;
} else {
this.parameterNames = "";
List<String> allParameterNames = context.getAllParameterNames();
if (allParameterNames.isEmpty()) {
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) {
@ -177,24 +238,26 @@ class QueryBlocks { @@ -177,24 +238,26 @@ class QueryBlocks {
if (StringUtils.hasText(source.getQuery().getFieldsString())) {
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());
}
String sortParameter = context.getSortParameterName();
if (StringUtils.hasText(sortParameter)) {
builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
} else if (StringUtils.hasText(source.getQuery().getSortString())) {
if (StringUtils.hasText(source.getQuery().getSortString())) {
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());
}
String sortParameter = context.getSortParameterName();
if (StringUtils.hasText(sortParameter)) {
builder.addStatement("$L.with($L)", queryVariableName, sortParameter);
}
String limitParameter = context.getLimitParameterName();
if (StringUtils.hasText(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());
}
@ -241,9 +304,15 @@ class QueryBlocks { @@ -241,9 +304,15 @@ class QueryBlocks {
org.springframework.data.mongodb.core.query.Collation.class, collationString);
} else {
builder.addStatement(
"$L.collation(collationOf(evaluate(ExpressionMarker.class.getEnclosingMethod(), $S$L)))",
queryVariableName, collationString, parameterNames);
if (getQueryParameters().isEmpty()) {
builder.addStatement(
"$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 { @@ -267,10 +336,13 @@ class QueryBlocks {
return CodeBlock.of("new $T(new $T())", BasicQuery.class, Document.class);
} else if (MongoCodeBlocks.containsPlaceholder(source)) {
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();
}
else {
} else {
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 @@ -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.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.util.Assert;
/**
* @author Christoph Strobl
@ -192,12 +193,17 @@ interface Snippet { @@ -192,12 +193,17 @@ interface Snippet {
@Override
public BuilderStyleBuilder with(Snippet snippet) {
Assert.notNull(targetMethodName, "TargetMethodName must be set before calling this method");
new BuilderStyleSnippet(targetVariableName, targetMethodName, snippet).appendTo(targetBuilder);
return this;
}
@Override
public VariableSnippet variable() {
Assert.notNull(variableSnippet, "VariableSnippet must not be null");
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> { @@ -205,11 +205,11 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
case STARTING_WITH:
case ENDING_WITH:
case CONTAINING:
return createContainingCriteria(part, property, criteria, parameters);
return createContainingCriteria(part, property, criteria, parameters.next());
case NOT_LIKE:
return createContainingCriteria(part, property, criteria.not(), parameters);
return createContainingCriteria(part, property, criteria.not(), parameters.next());
case NOT_CONTAINING:
return createContainingCriteria(part, property, criteria.not(), parameters);
return createContainingCriteria(part, property, criteria.not(), parameters.next());
case REGEX:
return regex(criteria, parameters.next());
case EXISTS:
@ -258,8 +258,9 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -258,8 +258,9 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
Iterator<Object> parameters) {
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 pointToUse = point == null ? nextAs(parameters, Point.class) : point;
@ -344,18 +345,17 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -344,18 +345,17 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
* @param part
* @param property
* @param criteria
* @param parameters
* @param parameter
* @return
*/
private Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria,
Iterator<Object> parameters) {
protected Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria,
Object parameter) {
if (property.isCollectionLike()) {
Object next = parameters.next();
return in(criteria, part, next);
return in(criteria, part, parameter);
}
return addAppropriateLikeRegexTo(criteria, part, parameters.next());
return addAppropriateLikeRegexTo(criteria, part, parameter);
}
/**
@ -375,6 +375,10 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -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()));
}
if(value instanceof Pattern pattern) {
return criteria.regex(pattern);
}
return criteria.regex(toLikeRegex(value.toString(), part), toRegexOptions(part));
}
@ -426,7 +430,6 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> { @@ -426,7 +430,6 @@ public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
streamable = streamable.map(it -> {
if (it instanceof String sv) {
return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions);
}
return it;

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

@ -202,7 +202,7 @@ public interface MongoQueryExecution { @@ -202,7 +202,7 @@ public interface MongoQueryExecution {
}
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.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric()));

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

@ -15,10 +15,31 @@ @@ -15,10 +15,31 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import static org.springframework.data.domain.Sort.Direction.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import static org.springframework.data.domain.Sort.Direction.ASC;
import static org.springframework.data.domain.Sort.Direction.DESC;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.DEFAULT_CONTEXT;
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.text.ParseException;
@ -46,7 +67,6 @@ import org.bson.Document; @@ -46,7 +67,6 @@ import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
@ -1579,7 +1599,8 @@ public class AggregationTests { @@ -1579,7 +1599,8 @@ public class AggregationTests {
createUsersWithReferencedPersons();
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"));
AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class);
@ -1598,8 +1619,10 @@ public class AggregationTests { @@ -1598,8 +1619,10 @@ public class AggregationTests {
createUsersWithReferencedPersons();
TypedAggregation<User> agg = newAggregation(User.class, //
lookup().from("person").localField("_id").foreignField("firstname").let(Let.ExpressionVariable.newVariable("the_id").forField("_id")).pipeline(
match(ctx -> new Document("$expr", new Document("$eq", List.of("$$the_id", "u1"))))).as("linkedPerson"),
lookup().from("person").localField("_id").foreignField("firstname")
.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"));
AggregationResults<Document> results = mongoTemplate.aggregate(agg, User.class, Document.class);
@ -1956,9 +1979,8 @@ public class AggregationTests { @@ -1956,9 +1979,8 @@ public class AggregationTests {
mongoTemplate.insert(objectToSave);
mongoTemplate.insert(objectToSave2);
Aggregation agg = Aggregation.newAggregation(
project().and(ArithmeticOperators.valueOf("x").percentile(0.9, 0.4).and("y").and("xField"))
.as("percentileValues"));
Aggregation agg = Aggregation.newAggregation(project()
.and(ArithmeticOperators.valueOf("x").percentile(0.9, 0.4).and("y").and("xField")).as("percentileValues"));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class);
@ -1979,8 +2001,7 @@ public class AggregationTests { @@ -1979,8 +2001,7 @@ public class AggregationTests {
mongoTemplate.insert(objectToSave2);
Aggregation agg = Aggregation.newAggregation(
project().and(ArithmeticOperators.valueOf("x").median().and("y").and("xField"))
.as("medianValue"));
project().and(ArithmeticOperators.valueOf("x").median().and("y").and("xField")).as("medianValue"));
AggregationResults<Document> result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class);
@ -2086,7 +2107,8 @@ public class AggregationTests { @@ -2086,7 +2107,8 @@ public class AggregationTests {
mongoTemplate.save(widget);
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);
}
@ -2097,9 +2119,7 @@ public class AggregationTests { @@ -2097,9 +2119,7 @@ public class AggregationTests {
Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build();
mongoTemplate.insert(Arrays.asList(item1, item2), Item.class);
TypedAggregation<Item> aggregation = newAggregation(Item.class,
match(where("itemId").is("1")),
unwind("tags"),
TypedAggregation<Item> aggregation = newAggregation(Item.class, match(where("itemId").is("1")), unwind("tags"),
match(where("itemId").is("1").and("tags").is("c")));
AggregationResults<Document> results = mongoTemplate.aggregate(aggregation, Document.class);
List<Document> mappedResults = results.getMappedResults();
@ -2114,9 +2134,7 @@ public class AggregationTests { @@ -2114,9 +2134,7 @@ public class AggregationTests {
Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build();
mongoTemplate.insert(Arrays.asList(item1, item2), Item.class);
TypedAggregation<Item> aggregation = newAggregation(Item.class,
match(where("itemId").is("1")),
unwind("tags"),
TypedAggregation<Item> aggregation = newAggregation(Item.class, match(where("itemId").is("1")), unwind("tags"),
project().and("itemId").as("itemId").and("tags").as("tags"),
match(where("itemId").is("1").and("tags").is("c")));
@ -3097,4 +3115,29 @@ public class AggregationTests { @@ -3097,4 +3115,29 @@ public class AggregationTests {
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 @@ -198,10 +198,16 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
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
void findByFirstnameLikeWithNull() {
assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameLike(null));
assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameLike((String)null));
}
@Test
@ -752,7 +758,6 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -752,7 +758,6 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
@Test // DATAMONGO-1608
void findByFirstNameIgnoreCaseWithNull() {
assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameIgnoreCase(null));
}
@ -1304,6 +1309,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -1304,6 +1309,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
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
void countsPersonsByFirstname() {
assertThat(repository.countByThePersonsFirstname("Dave")).isEqualTo(1L);
@ -1662,7 +1675,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie @@ -1662,7 +1675,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
@Test // GH-3656
@DirtiesState
void resultProjectionWithOptionalIsExcecutedCorrectly() {
void resultProjectionWithOptionalIsExecutedCorrectly() {
carter.setAddress(new Address("batman", "robin", "gotham"));
repository.save(carter);

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

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
package org.springframework.data.mongodb.repository;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -34,7 +34,6 @@ import org.springframework.test.context.ContextConfiguration; @@ -34,7 +34,6 @@ import org.springframework.test.context.ContextConfiguration;
* @author Mark Paluch
*/
@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 {
@Configuration
@ -61,4 +60,34 @@ class AotPersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegr @@ -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; @@ -28,7 +28,7 @@ import org.springframework.data.annotation.PersistenceCreator;
* @author Christoph Strobl
* @author Mark Paluch
*/
final class PersonAggregate {
public final class PersonAggregate {
@Id private final String lastname;
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 @@ -98,6 +98,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return
*/
List<Person> findByFirstnameLike(@Nullable String firstname);
List<Person> findByFirstnameLike(Pattern firstname);
List<Person> findByFirstnameNotContains(String firstname);
@ -108,8 +109,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query @@ -108,8 +109,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return
*/
List<Person> findByFirstnameNotLike(String firstname);
List<Person> findByFirstnameNotLike(Pattern firstname);
List<Person> findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort);
List<Person> findByFirstnameLikeOrderByLastnameAsc(Pattern firstname, Sort sort);
List<Person> findBySkillsContains(List<String> skills);
@ -128,8 +131,13 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query @@ -128,8 +131,13 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
ScrollPosition scrollPosition);
Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname,
ScrollPosition scrollPosition);
Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
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
@ -140,6 +148,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query @@ -140,6 +148,7 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return
*/
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).
@ -149,8 +158,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query @@ -149,8 +158,10 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @return
*/
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(Pattern lastname, Sort sort, Limit limit);
@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
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; @@ -20,7 +20,7 @@ import java.util.Objects;
/**
* @author Christoph Strobl
*/
final class SumAge {
public final class SumAge {
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; @@ -27,13 +27,17 @@ import org.springframework.beans.factory.config.RuntimeBeanReference;
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.env.StandardEnvironment;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mongodb.core.MongoOperations;
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.QueryMethodValueEvaluationContextAccessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.util.Lazy;
import org.springframework.util.ReflectionUtils;
/**
@ -124,6 +128,8 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce @@ -124,6 +128,8 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce
return new RepositoryFactoryBeanSupport.FragmentCreationContext() {
final Lazy<ProjectionFactory> projectionFactory = Lazy.of(SpelAwareProxyProjectionFactory::new);
@Override
public RepositoryMetadata getRepositoryMetadata() {
return repositoryContext.getRepositoryInformation();
@ -131,12 +137,15 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce @@ -131,12 +137,15 @@ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProce
@Override
public ValueExpressionDelegate getValueExpressionDelegate() {
return ValueExpressionDelegate.create();
QueryMethodValueEvaluationContextAccessor queryMethodValueEvaluationContextAccessor = new QueryMethodValueEvaluationContextAccessor(
new StandardEnvironment(), repositoryContext.getBeanFactory());
return new ValueExpressionDelegate(queryMethodValueEvaluationContextAccessor, ValueExpressionParser.create());
}
@Override
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 @@ -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`
* `@Hint`, `@Meta`, and `@ReadPreference` support
* `Page`, `Slice`, and `Optional` return types
* DTO Projections
* DTO & Interface Projections
**Limitations**
* `@Meta.allowDiskUse` and `flags` are not evaluated.
* Limited `Collation` detection.
* No support for in-clauses with pattern matching / case insensitivity
**Excluded methods**
* `CrudRepository` and other base interface methods
* Querydsl and Query by Example methods
* 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