diff --git a/pom.xml b/pom.xml index 9a1889723d..1cf8761bdc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5027-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..fb60c00423 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5027-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 595e5a4250..c41ad60ea5 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.x-GH-5027-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index a49cd4ae98..895288d889 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -111,9 +111,11 @@ 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 @@ protected AggregationResults doAggregate(Aggregation aggregation, String AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, QueryResultConverter resultConverter, AggregationOperationContext context) { - DocumentCallback callback = new QueryResultConverterCallback<>(resultConverter, + final DocumentCallback callback; + if(aggregation instanceof TypedAggregation ta && outputType.isInterface()) { + EntityProjection 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); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index 602713cb77..24e2c99a63 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -377,6 +377,9 @@ Document getMappedFields(@Nullable MongoPersistentEntity entity, EntityProjec 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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java index e8dbffb19a..c376fb11fd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationBlocks.java @@ -22,13 +22,13 @@ 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 @@ CodeBlock build() { 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 @@ CodeBlock build() { 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 @@ CodeBlock 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 @@ -173,13 +178,7 @@ static class AggregationCodeBlockBuilder { 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 @@ private CodeBlock pipeline(String pipelineVariableName) { 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 @@ private CodeBlock pipeline(String pipelineVariableName) { builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, context.localVariable("stages")); + return builder.build(); } @@ -303,7 +304,7 @@ private CodeBlock aggregationStages(String stageListVariableName, Collection outputType) { Builder builder = CodeBlock.builder(); @@ -322,8 +323,17 @@ private CodeBlock sortingStage(String sortProvider) { 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 @@ private CodeBlock pagingStage(String pageableProvider, boolean slice) { 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); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index be30dcf357..b6cbe50833 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -17,6 +17,7 @@ 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 @@ static Placeholder indexed(int position) { * @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 @@ public static Shape geoJson(int index, String type) { * @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 @@ public static Point point(int index) { * @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 @@ public static Shape circle(int index) { * @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 @@ public static Shape box(int index) { * @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 @@ public static Shape sphere(int index) { * @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 @@ private static class PointPlaceholder extends Point implements Placeholder { private final int index; - public PointPlaceholder(int index) { + PointPlaceholder(int index) { super(Double.NaN, Double.NaN); this.index = index; } @@ -184,7 +188,7 @@ private static class CirclePlaceholder extends Circle implements Placeholder { 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 @@ private static class BoxPlaceholder extends Box implements Placeholder { 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 @@ private static class SpherePlaceholder extends Sphere implements Placeholder { 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 @@ private static class PolygonPlaceholder extends Polygon implements Placeholder { 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 @@ public String toString() { } + 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(); + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index d48983ba7e..b0c998e652 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -19,11 +19,11 @@ 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.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.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 @@ AotStringQuery createQuery(PartTree partTree, QueryMethod queryMethod, Method so ? 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 @@ protected Criteria regex(Criteria criteria, Object param) { 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 @@ static class PlaceholderParameterAccessor implements MongoParameterAccessor { private final List placeholders; - public PlaceholderParameterAccessor(QueryMethod queryMethod) { + @Nullable Part getPartForIndex(PartTree partTree, Parameter parameter) { + if(!parameter.isBindable()) { + return null; + } + + List 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 @@ public PlaceholderParameterAccessor(QueryMethod queryMethod) { 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 @@ public boolean hasBindableNullValue() { public Iterator iterator() { return ((List) placeholders).iterator(); } + + public List getPlaceholders() { + return placeholders; + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java index 01373c2fb0..c0920ba771 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java +++ b/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; +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.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 { private @Nullable String sort; private @Nullable String fields; - public AotStringQuery(Query query) { + private List placeholders = new ArrayList<>(); + + public AotStringQuery(Query query, List placeholders) { this.delegate = query; + this.placeholders = placeholders; } public AotStringQuery(String query) { @@ -72,6 +79,22 @@ public Query sort(String sort) { 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(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index b7f65df1a8..83c96e5494 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -17,14 +17,16 @@ 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.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.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 @@ protected Document bindParameters(Method method, String source, Object... args) ParameterBindingContext bindingContext = new ParameterBindingContext(parametersParameterAccessor::getBindableValue, new ValueExpressionEvaluator() { - @Override - @SuppressWarnings("unchecked") - public @Nullable T evaluate(String expression) { + @Override + @SuppressWarnings("unchecked") + public @Nullable T evaluate(String expression) { return (T) MongoAotRepositoryFragmentSupport.this.evaluate(method, expression, args); - } - }); + } + }); return CODEC.decode(source, bindingContext); } @@ -224,33 +229,72 @@ protected Collation collationOf(@Nullable Object 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) { Document queryDocument = bindParameters(method, queryString, parameters); return new BasicQuery(queryDocument); } + @SuppressWarnings("NullAway") protected AggregationPipeline createPipeline(List rawStages) { - List 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 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 convertSimpleRawResults(Class targetType, List rawResults) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java index e887e0a118..ee74c76eca 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -19,7 +19,6 @@ 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; @@ -33,6 +32,7 @@ import org.springframework.data.mongodb.repository.aot.UpdateBlocks.UpdateExecutionCodeBlockBuilder; import org.springframework.data.mongodb.repository.query.MongoQueryMethod; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; +import org.springframework.data.repository.aot.generate.ExpressionMarker; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.util.NumberUtils; @@ -163,21 +163,28 @@ static GeoNearExecutionCodeBlockBuilder geoNearExecutionBlockBuilder(AotQueryMet return new GeoNearExecutionCodeBlockBuilder(context); } - static CodeBlock asDocument(String source, String argNames) { + static CodeBlock asDocument(ExpressionMarker expressionMarker, String source, String argNames) { + return asDocument(expressionMarker, source, CodeBlock.of("$L", argNames)); + } + + static CodeBlock asDocument(ExpressionMarker expressionMarker, 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($L, $S)", expressionMarker.enclosingMethod(), source); + } else { + builder.add("bindParameters($L, $S, $L)", expressionMarker.enclosingMethod(), 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)) { @@ -207,7 +214,7 @@ static CodeBlock evaluateNumberPotentially(String value, Class } Builder builder = CodeBlock.builder(); - builder.add("($T) evaluate(ExpressionMarker.class.getEnclosingMethod(), $S$L)", targetType, value, + builder.add("($T) evaluate($L, $S$L)", targetType, context.getExpressionMarker().enclosingMethod(), value, parameterNames); return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index 96bc359241..0da74d06f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -93,6 +93,7 @@ public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { 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; @@ -215,7 +216,7 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB CodeBlock.Builder builder = CodeBlock.builder(); - builder.addStatement("class ExpressionMarker{}"); +// builder.addStatement("class ExpressionMarker{}"); builder.add(finalContribution.contribute(context)); return builder.build(); @@ -237,7 +238,8 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor } 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()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index 0f3697b303..391e6cc07f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/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; +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.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 @@ CodeBlock build() { } 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 @@ CodeBlock build() { 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 @@ static class QueryCodeBlockBuilder { private final AotQueryMethodGenerationContext context; private final MongoQueryMethod queryMethod; - private final String parameterNames; + private final Lazy queryParameters; private QueryInteraction source; private String queryVariableName; @@ -146,14 +167,54 @@ static class QueryCodeBlockBuilder { 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 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 @@ CodeBlock build() { 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(context.getExpressionMarker(), 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(context.getExpressionMarker(), 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 @@ CodeBlock build() { 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($L, $S)))", + queryVariableName, context.getExpressionMarker().enclosingMethod(), collationString); + } else { + builder.addStatement( + "$L.collation(collationOf(evaluate($L, $S, $L)))", + queryVariableName, context.getExpressionMarker().enclosingMethod(), collationString, getQueryParameters()); + } } } } @@ -267,10 +336,13 @@ private CodeBlock renderExpressionToQuery() { 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($L, $S)", context.getExpressionMarker().enclosingMethod(), source); + } else { + builder.add("createQuery($L, $S, $L)", context.getExpressionMarker().enclosingMethod(), source, getQueryParameters()); + } return builder.build(); - } - else { + } else { return CodeBlock.of("new $T(parse($S))", BasicQuery.class, source); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java index 2518be58b0..9fafdb1712 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/Snippet.java +++ b/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.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 @@ public BuilderStyleMethodArgumentBuilder call(String methodName) { @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; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 71d51f16e8..807dc518d3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -205,11 +205,11 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit 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 @@ private Criteria createNearCriteria(MongoPersistentProperty property, Criteria c Iterator parameters) { Range range = accessor.getDistanceRange(); - Optional distance = range.getUpperBound().getValue(); - Optional minDistance = range.getLowerBound().getValue(); + + Optional distance = range != null ? range.getUpperBound().getValue() : Optional.empty(); + Optional 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 @@ private Criteria createLikeRegexCriteriaOrThrow(Part part, MongoPersistentProper * @param part * @param property * @param criteria - * @param parameters + * @param parameter * @return */ - private Criteria createContainingCriteria(Part part, MongoPersistentProperty property, Criteria criteria, - Iterator 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 @@ private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, @Nullab "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 @@ private java.util.List valueAsList(Object value, Part part) { streamable = streamable.map(it -> { if (it instanceof String sv) { - return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions); } return it; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index 263b1ef8de..538e05df58 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -202,7 +202,7 @@ NearQuery nearQuery(Query query) { } Range 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())); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 50e191500a..c1d941036d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/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; -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.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; @@ -554,7 +574,7 @@ void findStatesWithPopulationOver10MillionAggregationExample() { /* //complex mongodb aggregation framework example from https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state - + db.zipcodes.aggregate( { $group: { @@ -1579,7 +1599,8 @@ void shouldLookupPeopleCorrectlyWithPipeline() { createUsersWithReferencedPersons(); TypedAggregation 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 results = mongoTemplate.aggregate(agg, User.class, Document.class); @@ -1598,8 +1619,10 @@ void shouldLookupPeopleCorrectlyWithPipelineAndLet() { createUsersWithReferencedPersons(); TypedAggregation 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 results = mongoTemplate.aggregate(agg, User.class, Document.class); @@ -1956,9 +1979,8 @@ void percentileShouldBeAppliedCorrectly() { 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 result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); @@ -1979,8 +2001,7 @@ void medianShouldBeAppliedCorrectly() { 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 result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); @@ -2086,7 +2107,8 @@ void considersMongoIdWithinTypedCollections() { mongoTemplate.save(widget); Criteria criteria = Criteria.where("users").elemMatch(Criteria.where("id").is("4ee921aca44fd11b3254e001")); - AggregationResults aggregate = mongoTemplate.aggregate(newAggregation(match(criteria)), Widget.class, Widget.class); + AggregationResults aggregate = mongoTemplate.aggregate(newAggregation(match(criteria)), Widget.class, + Widget.class); assertThat(aggregate.getMappedResults()).contains(widget); } @@ -2097,9 +2119,7 @@ void shouldHonorFieldAliasesForFieldReferencesUsingFieldExposingOperation() { Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build(); mongoTemplate.insert(Arrays.asList(item1, item2), Item.class); - TypedAggregation aggregation = newAggregation(Item.class, - match(where("itemId").is("1")), - unwind("tags"), + TypedAggregation aggregation = newAggregation(Item.class, match(where("itemId").is("1")), unwind("tags"), match(where("itemId").is("1").and("tags").is("c"))); AggregationResults results = mongoTemplate.aggregate(aggregation, Document.class); List mappedResults = results.getMappedResults(); @@ -2114,9 +2134,7 @@ void projectShouldResetContextToAvoidMappingFieldsAgainstANoLongerExistingTarget Item item2 = Item.builder().itemId("1").tags(Arrays.asList("a", "c")).build(); mongoTemplate.insert(Arrays.asList(item1, item2), Item.class); - TypedAggregation aggregation = newAggregation(Item.class, - match(where("itemId").is("1")), - unwind("tags"), + TypedAggregation 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 String toString() { 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 agg = newAggregation(MyOhMy.class, project("firstname")); + AggregationResults 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(); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 493a23e4e5..c361d5513d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -198,10 +198,16 @@ void findsPersonsByFirstnameLike() { assertThat(result).hasSize(1).contains(boyd); } + @Test // GH-5027 + void findsPersonsByFirstnameLikeWithPattern() { + + List 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 @@ void executesGeoPageCountCorrectly() { @Test // DATAMONGO-1608 void findByFirstNameIgnoreCaseWithNull() { - assertThatIllegalArgumentException().isThrownBy(() -> repository.findByFirstnameIgnoreCase(null)); } @@ -1304,6 +1309,14 @@ void findsPersonsByFirstnameNotLike() { assertThat(result).doesNotContain(boyd); } + @Test // GH-5027 + void findsPersonsByFirstnameNotLikeWithPattern() { + + List 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 @@ void executesQueryWithDocumentReferenceCorrectly() { @Test // GH-3656 @DirtiesState - void resultProjectionWithOptionalIsExcecutedCorrectly() { + void resultProjectionWithOptionalIsExecutedCorrectly() { carter.setAddress(new Address("batman", "robin", "gotham")); repository.save(carter); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index ce26b11c5e..bac2bde266 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -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 @@ * @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 @@ PersonRepository personRepository(MongoOperations mongoOperations, ApplicationCo } + @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(); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java index da22801ba6..9120042eca 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java @@ -28,7 +28,7 @@ * @author Christoph Strobl * @author Mark Paluch */ -final class PersonAggregate { +public final class PersonAggregate { @Id private final String lastname; private final Set names; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 9ab0d71dc3..a9f694e200 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -98,6 +98,7 @@ public interface PersonRepository extends MongoRepository, Query * @return */ List findByFirstnameLike(@Nullable String firstname); + List findByFirstnameLike(Pattern firstname); List findByFirstnameNotContains(String firstname); @@ -108,8 +109,10 @@ public interface PersonRepository extends MongoRepository, Query * @return */ List findByFirstnameNotLike(String firstname); + List findByFirstnameNotLike(Pattern firstname); List findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort); + List findByFirstnameLikeOrderByLastnameAsc(Pattern firstname, Sort sort); List findBySkillsContains(List skills); @@ -128,8 +131,13 @@ public interface PersonRepository extends MongoRepository, Query Window findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition); + Window findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(Pattern lastname, + ScrollPosition scrollPosition); + Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition, Limit limit); + Window 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 @@ Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, * @return */ Window findCursorProjectionByLastnameLike(String lastname, Pageable pageable); + Window 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 @@ Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, * @return */ Page findByLastnameLike(String lastname, Pageable pageable); + Page findByLastnameLike(Pattern lastname, Pageable pageable); List findByLastnameLike(String lastname, Sort sort, Limit limit); + List findByLastnameLike(Pattern lastname, Sort sort, Limit limit); @Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}") Page findByLastnameLikeWithPageable(String lastname, Pageable pageable); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java index abbfac5943..c98c33d270 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SumAge.java @@ -20,7 +20,7 @@ /** * @author Christoph Strobl */ -final class SumAge { +public final class SumAge { private final Long total; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java index f2feeac4ba..ef44c2da8a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java @@ -27,13 +27,17 @@ 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 @@ private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( return new RepositoryFactoryBeanSupport.FragmentCreationContext() { + final Lazy projectionFactory = Lazy.of(SpelAwareProxyProjectionFactory::new); + @Override public RepositoryMetadata getRepositoryMetadata() { return repositoryContext.getRepositoryInformation(); @@ -131,12 +137,15 @@ public RepositoryMetadata getRepositoryMetadata() { @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(); } }; } diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 0bc943eafd..fbb189a66b 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -21,7 +21,7 @@ + level="trace"/> diff --git a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc index 0c496c90f0..991581c8e2 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc +++ b/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` * `@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 findByLastnameLike(Pattern lastname); +---- +====