Compare commits

..

10 Commits

Author SHA1 Message Date
Mark Paluch
7103ca2228 Polishing.
Reformatting, trailing whitespaces.
2023-07-10 10:15:30 +02:00
Christoph Strobl
b46aab2c28 Map collection and fields for $graphLookup aggregation against type.
This commit enables using a type parameter to define the from collection of a graphLookup aggregation stage. In doing so we can derive the target collection name from the type and use the given information to also map the from field against the domain object to so that the user is able to operate on property names instead of the target db field name.
2023-07-10 09:47:06 +02:00
Christoph Strobl
ff137eca8a Map collection and fields for $lookup aggregation against type.
This commit enables using a type parameter to define the from collection of a lookup aggregation stage. In doing so we can derive the target collection name from the type and use the given information to also map the from field against the domain object to so that the user is able to operate on property names instead of the target db field name.
2023-07-10 09:47:06 +02:00
Christoph Strobl
a93c870b45 Prepare issue branch. 2023-07-10 09:47:06 +02:00
Mark Paluch
af26bb6b31 Polishing.
Introduce limit(Limit) method to limit query results applying the Limit domain type.

See #4397
Original pull request: #4398
2023-07-05 12:02:04 +02:00
Christoph Strobl
d78f47f035 Add tests to verify Limit is supported.
Closes #4397
Original pull request: #4398
2023-07-05 12:01:44 +02:00
Mark Paluch
8cd956e90a Update CI properties.
See #4387
2023-07-03 09:50:16 +02:00
Mark Paluch
49cc6a708d Upgrade to Maven Wrapper 3.9.3.
See #4436
2023-07-03 09:49:43 +02:00
Christoph Strobl
0bf472a29b Polishing.
Update tests to make use of ValueSource.
Replace regex based path inspection with segment by segment analysis.

Original Pull Request: #4427
2023-06-28 13:25:08 +02:00
lijixue
2de00cdb2f Fix QueryMapper property path resolution for nested paths containing numeric values.
Prior to this fix a path that contains numeric values used as position parameters would have been stripped in a way that left out the last digit. This could lead to wrong path resolution if the incorrectly constructed property name accidentally matched an existing one.

Closes: #4426
Original Pull Request: #4427
2023-06-28 13:25:08 +02:00
21 changed files with 455 additions and 49 deletions

View File

@@ -1,2 +1,2 @@
#Tue Jun 13 08:54:58 CEST 2023
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip
#Mon Jul 03 09:49:43 CEST 2023
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip

View File

@@ -1,5 +1,5 @@
# Java versions
java.main.tag=17.0.6_10-jdk-focal
java.main.tag=17.0.7_7-jdk-focal
java.next.tag=20-jdk-jammy
# Docker container images - standard
@@ -7,15 +7,15 @@ docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/ecli
docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag}
# Supported versions of MongoDB
docker.mongodb.4.4.version=4.4.18
docker.mongodb.5.0.version=5.0.14
docker.mongodb.6.0.version=6.0.4
docker.mongodb.4.4.version=4.4.22
docker.mongodb.5.0.version=5.0.18
docker.mongodb.6.0.version=6.0.7
# Supported versions of Redis
docker.redis.6.version=6.2.10
docker.redis.6.version=6.2.12
# Supported versions of Cassandra
docker.cassandra.3.version=3.11.14
docker.cassandra.3.version=3.11.15
# Docker environment settings
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4426-SNAPSHOT</version>
<version>4.2.x-4379-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4426-SNAPSHOT</version>
<version>4.2.x-4379-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4426-SNAPSHOT</version>
<version>4.2.x-4379-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4426-SNAPSHOT</version>
<version>4.2.x-4379-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -23,6 +23,7 @@ import org.bson.Document;
import org.bson.codecs.configuration.CodecRegistry;
import org.springframework.beans.BeanUtils;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.MongoCollectionUtils;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -79,7 +80,30 @@ public interface AggregationOperationContext extends CodecRegistryProvider {
FieldReference getReference(String name);
/**
* Returns the {@link Fields} exposed by the type. May be a {@literal class} or an {@literal interface}. The default
* Obtain the target field name for a given field/type combination.
*
* @param type The type containing the field.
* @param field The property/field name
* @return never {@literal null}.
* @since 4.2
*/
default String getMappedFieldName(Class<?> type, String field) {
return field;
}
/**
* Obtain the collection name for a given {@link Class type} combination.
*
* @param type
* @return never {@literal null}.
* @since 4.2
*/
default String getCollection(Class<?> type) {
return MongoCollectionUtils.getPreferredCollectionName(type);
}
/**
* Returns the {@link Fields} exposed by the type. Can be a {@literal class} or an {@literal interface}. The default
* implementation uses {@link BeanUtils#getPropertyDescriptors(Class) property descriptors} discover fields from a
* {@link Class}.
*
@@ -109,7 +133,7 @@ public interface AggregationOperationContext extends CodecRegistryProvider {
/**
* This toggle allows the {@link AggregationOperationContext context} to use any given field name without checking for
* its existence. Typically the {@link AggregationOperationContext} fails when referencing unknown fields, those that
* its existence. Typically, the {@link AggregationOperationContext} fails when referencing unknown fields, those that
* are not present in one of the previous stages or the input source, throughout the pipeline.
*
* @return a more relaxed {@link AggregationOperationContext}.

View File

@@ -46,7 +46,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
private static final Set<Class<?>> ALLOWED_START_TYPES = new HashSet<Class<?>>(
Arrays.<Class<?>> asList(AggregationExpression.class, String.class, Field.class, Document.class));
private final String from;
private final Object from;
private final List<Object> startWith;
private final Field connectFrom;
private final Field connectTo;
@@ -55,7 +55,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
private final @Nullable Field depthField;
private final @Nullable CriteriaDefinition restrictSearchWithMatch;
private GraphLookupOperation(String from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
private GraphLookupOperation(Object from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
@Nullable Long maxDepth, @Nullable Field depthField, @Nullable CriteriaDefinition restrictSearchWithMatch) {
this.from = from;
@@ -82,7 +82,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
Document graphLookup = new Document();
graphLookup.put("from", from);
graphLookup.put("from", getCollectionName(context));
List<Object> mappedStartWith = new ArrayList<>(startWith.size());
@@ -99,7 +99,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
graphLookup.put("connectFromField", connectFrom.getTarget());
graphLookup.put("connectFromField", getForeignFieldName(context));
graphLookup.put("connectToField", connectTo.getTarget());
graphLookup.put("as", as.getName());
@@ -118,6 +118,16 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
return new Document(getOperator(), graphLookup);
}
String getCollectionName(AggregationOperationContext context) {
return from instanceof Class<?> type ? context.getCollection(type) : from.toString();
}
String getForeignFieldName(AggregationOperationContext context) {
return from instanceof Class<?> type ? context.getMappedFieldName(type, connectFrom.getTarget())
: connectFrom.getTarget();
}
@Override
public String getOperator() {
return "$graphLookup";
@@ -128,7 +138,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
List<ExposedField> fields = new ArrayList<>(2);
fields.add(new ExposedField(as, true));
if(depthField != null) {
if (depthField != null) {
fields.add(new ExposedField(depthField, true));
}
return ExposedFields.from(fields.toArray(new ExposedField[0]));
@@ -146,6 +156,17 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
* @return never {@literal null}.
*/
StartWithBuilder from(String collectionName);
/**
* Use the given type to determine name of the foreign collection and map
* {@link ConnectFromBuilder#connectFrom(String)} against it to consider eventually present
* {@link org.springframework.data.mongodb.core.mapping.Field} annotations.
*
* @param type must not be {@literal null}.
* @return never {@literal null}.
* @since 4.2
*/
StartWithBuilder from(Class<?> type);
}
/**
@@ -218,7 +239,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
static final class GraphLookupOperationFromBuilder
implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder {
private @Nullable String from;
private @Nullable Object from;
private @Nullable List<? extends Object> startWith;
private @Nullable String connectFrom;
@@ -231,6 +252,14 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
return this;
}
@Override
public StartWithBuilder from(Class<?> type) {
Assert.notNull(type, "Type must not be null");
this.from = type;
return this;
}
@Override
public ConnectFromBuilder startWith(String... fieldReferences) {
@@ -321,7 +350,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
*/
public static final class GraphLookupOperationBuilder {
private final String from;
private final Object from;
private final List<Object> startWith;
private final Field connectFrom;
private final Field connectTo;
@@ -329,7 +358,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
private @Nullable Field depthField;
private @Nullable CriteriaDefinition restrictSearchWithMatch;
protected GraphLookupOperationBuilder(String from, List<? extends Object> startWith, String connectFrom,
protected GraphLookupOperationBuilder(Object from, List<? extends Object> startWith, String connectFrom,
String connectTo) {
this.from = from;

View File

@@ -39,7 +39,7 @@ import org.springframework.util.Assert;
*/
public class LookupOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation {
private final String from;
private Object from;
@Nullable //
private final Field localField;
@@ -97,6 +97,22 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
*/
public LookupOperation(String from, @Nullable Field localField, @Nullable Field foreignField, @Nullable Let let,
@Nullable AggregationPipeline pipeline, Field as) {
this((Object) from, localField, foreignField, let, pipeline, as);
}
/**
* Creates a new {@link LookupOperation} for the given combination of {@link Field}s and {@link AggregationPipeline
* pipeline}.
*
* @param from must not be {@literal null}. Can be eiter the target collection name or a {@link Class}.
* @param localField can be {@literal null} if {@literal pipeline} is present.
* @param foreignField can be {@literal null} if {@literal pipeline} is present.
* @param let can be {@literal null} if {@literal localField} and {@literal foreignField} are present.
* @param as must not be {@literal null}.
* @since 4.2
*/
private LookupOperation(Object from, @Nullable Field localField, @Nullable Field foreignField, @Nullable Let let,
@Nullable AggregationPipeline pipeline, Field as) {
Assert.notNull(from, "From must not be null");
if (pipeline == null) {
@@ -125,12 +141,14 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
Document lookupObject = new Document();
lookupObject.append("from", from);
lookupObject.append("from", getCollectionName(context));
if (localField != null) {
lookupObject.append("localField", localField.getTarget());
}
if (foreignField != null) {
lookupObject.append("foreignField", foreignField.getTarget());
lookupObject.append("foreignField", getForeignFieldName(context));
}
if (let != null) {
lookupObject.append("let", let.toDocument(context).get("$let", Document.class).get("vars"));
@@ -144,6 +162,16 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
return new Document(getOperator(), lookupObject);
}
String getCollectionName(AggregationOperationContext context) {
return from instanceof Class<?> type ? context.getCollection(type) : from.toString();
}
String getForeignFieldName(AggregationOperationContext context) {
return from instanceof Class<?> type ? context.getMappedFieldName(type, foreignField.getTarget())
: foreignField.getTarget();
}
@Override
public String getOperator() {
return "$lookup";
@@ -158,16 +186,28 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
return new LookupOperationBuilder();
}
public static interface FromBuilder {
public interface FromBuilder {
/**
* @param name the collection in the same database to perform the join with, must not be {@literal null} or empty.
* @return never {@literal null}.
*/
LocalFieldBuilder from(String name);
/**
* Use the given type to determine name of the foreign collection and map
* {@link ForeignFieldBuilder#foreignField(String)} against it to consider eventually present
* {@link org.springframework.data.mongodb.core.mapping.Field} annotations.
*
* @param type the type of the target collection in the same database to perform the join with, must not be
* {@literal null}.
* @return never {@literal null}.
* @since 4.2
*/
LocalFieldBuilder from(Class<?> type);
}
public static interface LocalFieldBuilder extends PipelineBuilder {
public interface LocalFieldBuilder extends PipelineBuilder {
/**
* @param name the field from the documents input to the {@code $lookup} stage, must not be {@literal null} or
@@ -177,7 +217,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
ForeignFieldBuilder localField(String name);
}
public static interface ForeignFieldBuilder {
public interface ForeignFieldBuilder {
/**
* @param name the field from the documents in the {@code from} collection, must not be {@literal null} or empty.
@@ -246,7 +286,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
LookupOperation as(String name);
}
public static interface AsBuilder extends PipelineBuilder {
public interface AsBuilder extends PipelineBuilder {
/**
* @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
@@ -264,7 +304,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
public static final class LookupOperationBuilder
implements FromBuilder, LocalFieldBuilder, ForeignFieldBuilder, AsBuilder {
private @Nullable String from;
private @Nullable Object from;
private @Nullable Field localField;
private @Nullable Field foreignField;
private @Nullable ExposedField as;
@@ -288,6 +328,14 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
return this;
}
@Override
public LocalFieldBuilder from(Class<?> type) {
Assert.notNull(type, "'From' must not be null");
from = type;
return this;
}
@Override
public AsBuilder foreignField(String name) {

View File

@@ -30,9 +30,8 @@ import org.springframework.lang.Nullable;
/**
* {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix.
* Useful when mapping fields to domain specific types while having to prefix keys for query purpose.
* <br />
* Fields to be excluded from prefixing my be added to a {@literal denylist}.
* Useful when mapping fields to domain specific types while having to prefix keys for query purpose. <br />
* Fields to be excluded from prefixing can be added to a {@literal denylist}.
*
* @author Christoph Strobl
* @author Mark Paluch

View File

@@ -92,6 +92,20 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
return getReferenceFor(field(name));
}
@Override
public String getCollection(Class<?> type) {
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(type);
return persistentEntity != null ? persistentEntity.getCollection() : AggregationOperationContext.super.getCollection(type);
}
@Override
public String getMappedFieldName(Class<?> type, String field) {
PersistentPropertyPath<MongoPersistentProperty> persistentPropertyPath = mappingContext.getPersistentPropertyPath(field, type);
return persistentPropertyPath.getLeafProperty().getFieldName();
}
@Override
public Fields getFields(Class<?> type) {

View File

@@ -1089,6 +1089,7 @@ public class QueryMapper {
protected static class MetadataBackedField extends Field {
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
private static final Pattern NUMERIC_SEGMENT = Pattern.compile("\\d+");
private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s; Associations can only be pointed to directly or via their id property";
private final MongoPersistentEntity<?> entity;
@@ -1338,22 +1339,24 @@ public class QueryMapper {
return source;
}
List<String> path = new ArrayList<>();
List<String> path = new ArrayList<>(segments.length);
/* always start from a property, so we can skip the first segment.
from there remove any position placeholder */
for (String segment : Arrays.copyOfRange(segments, 1, segments.length)) {
for(int i=1; i < segments.length; i++) {
String segment = segments[i];
if (segment.startsWith("[") && segment.endsWith("]")) {
continue;
}
if (segment.matches("\\d+")) {
if (NUMERIC_SEGMENT.matcher(segment).matches()) {
continue;
}
path.add(segment);
}
// when property is followed only by placeholders eg. 'values.0.3.90'
if (path.isEmpty()) {
// or when there is no difference in the number of segments
if (path.isEmpty() || segments.length == path.size() + 1) {
return source;
}

View File

@@ -31,6 +31,7 @@ import java.util.Set;
import org.bson.Document;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
@@ -66,7 +67,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
private @Nullable Field fieldSpec = null;
private Sort sort = Sort.unsorted();
private long skip;
private int limit;
private Limit limit = Limit.unlimited();
private KeysetScrollPosition keysetScrollPosition;
private @Nullable ReadConcern readConcern;
@@ -155,10 +156,30 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
* @return this.
*/
public Query limit(int limit) {
this.limit = limit;
this.limit = limit > 0 ? Limit.of(limit) : Limit.unlimited();
return this;
}
/**
* Limit the number of returned documents to {@link Limit}.
*
* @param limit number of documents to return.
* @return this.
* @since 4.2
*/
public Query limit(Limit limit) {
Assert.notNull(limit, "Limit must not be null");
if (limit.isUnlimited()) {
this.limit = limit;
return this;
}
// retain zero/negative semantics for unlimited.
return limit(limit.max());
}
/**
* Configures the query to use the given hint when being executed. The {@code hint} can either be an index name or a
* json {@link Document} representation.
@@ -254,7 +275,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
return this;
}
this.limit = pageable.getPageSize();
this.limit = pageable.toLimit();
this.skip = pageable.getOffset();
return with(pageable.getSort());
@@ -457,7 +478,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
* @since 4.1
*/
public boolean isLimited() {
return this.limit > 0;
return this.limit.isLimited();
}
/**
@@ -468,7 +489,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
* @see #isLimited()
*/
public int getLimit() {
return this.limit;
return limit.isUnlimited() ? 0 : this.limit.max();
}
/**
@@ -683,7 +704,8 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
};
target.skip = source.getSkip();
target.limit = source.getLimit();
target.limit = source.isLimited() ? Limit.of(source.getLimit()) : Limit.unlimited();
target.hint = source.getHint();
target.collation = source.getCollation();
target.restrictedTypes = new HashSet<>(source.getRestrictedTypes());
@@ -746,7 +768,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
result += 31 * nullSafeHashCode(sort);
result += 31 * nullSafeHashCode(hint);
result += 31 * skip;
result += 31 * limit;
result += 31 * limit.hashCode();
result += 31 * nullSafeHashCode(meta);
result += 31 * nullSafeHashCode(collation.orElse(null));

View File

@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.ScrollPosition;
@@ -117,6 +118,11 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor {
return delegate.getUpdate();
}
@Override
public Limit getLimit() {
return delegate.getLimit();
}
/**
* Converts the given value with the underlying {@link MongoWriter}.
*

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2023. the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.aggregation;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
/**
* @author Christoph Strobl
*/
public final class AggregationTestUtils {
public static AggregationContextBuilder<TypeBasedAggregationOperationContext> strict(Class<?> type) {
AggregationContextBuilder<AggregationOperationContext> builder = new AggregationContextBuilder<>();
builder.strict = true;
return builder.forType(type);
}
public static AggregationContextBuilder<TypeBasedAggregationOperationContext> relaxed(Class<?> type) {
AggregationContextBuilder<AggregationOperationContext> builder = new AggregationContextBuilder<>();
builder.strict = false;
return builder.forType(type);
}
public static class AggregationContextBuilder<T extends AggregationOperationContext> {
Class<?> targetType;
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
QueryMapper queryMapper;
boolean strict;
public AggregationContextBuilder<TypeBasedAggregationOperationContext> forType(Class<?> type) {
this.targetType = type;
return (AggregationContextBuilder<TypeBasedAggregationOperationContext>) this;
}
public AggregationContextBuilder<T> using(
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
return this;
}
public AggregationContextBuilder<T> using(QueryMapper queryMapper) {
this.queryMapper = queryMapper;
return this;
}
public T ctx() {
//
if (targetType == null) {
return (T) Aggregation.DEFAULT_CONTEXT;
}
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> ctx = mappingContext != null
? mappingContext
: MongoTestMappingContext.newTestContext().init();
QueryMapper qm = queryMapper != null ? queryMapper
: new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, ctx));
return (T) (strict ? new TypeBasedAggregationOperationContext(targetType, ctx, qm)
: new RelaxedTypeBasedAggregationOperationContext(targetType, ctx, qm));
}
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Arrays;
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.core.Person;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria;
/**
@@ -34,7 +35,7 @@ public class GraphLookupOperationUnitTests {
@Test // DATAMONGO-1551
public void rejectsNullFromCollection() {
assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from(null));
assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from((String) null));
}
@Test // DATAMONGO-1551
@@ -158,4 +159,59 @@ public class GraphLookupOperationUnitTests {
assertThat(document).containsEntry("$graphLookup.depthField", "foo.bar");
}
@Test // GH-4379
void unmappedLookupWithFromExtractedFromType() {
GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
.from(Employee.class) //
.startWith(LiteralOperators.Literal.asLiteral("hello")) //
.connectFrom("manager") //
.connectTo("name") //
.as("reportingHierarchy");
assertThat(graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
{ $graphLookup:
{
from: "employee",
startWith : { $literal : "hello" },
connectFromField: "manager",
connectToField: "name",
as: "reportingHierarchy"
}
}}
""");
}
@Test // GH-4379
void mappedLookupWithFromExtractedFromType() {
GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
.from(Employee.class) //
.startWith(LiteralOperators.Literal.asLiteral("hello")) //
.connectFrom("manager") //
.connectTo("name") //
.as("reportingHierarchy");
assertThat(graphLookupOperation.toDocument(AggregationTestUtils.strict(Employee.class).ctx())).isEqualTo("""
{ $graphLookup:
{
from: "employees",
startWith : { $literal : "hello" },
connectFromField: "reportsTo",
connectToField: "name",
as: "reportingHierarchy"
}
}}
""");
}
@org.springframework.data.mongodb.core.mapping.Document("employees")
static class Employee {
String id;
@Field("reportsTo")
String manager;
}
}

View File

@@ -25,6 +25,7 @@ import java.util.List;
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria;
/**
@@ -92,7 +93,7 @@ public class LookupOperationUnitTests {
@Test // DATAMONGO-1326
public void builderRejectsNullFromField() {
assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from(null));
assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from((String) null));
}
@Test // DATAMONGO-1326
@@ -195,10 +196,10 @@ public class LookupOperationUnitTests {
void buildsLookupWithLocalAndForeignFieldAsWellAsLetAndPipeline() {
LookupOperation lookupOperation = Aggregation.lookup().from("restaurants") //
.localField("restaurant_name")
.foreignField("name")
.localField("restaurant_name") //
.foreignField("name") //
.let(newVariable("orders_drink").forField("drink")) //
.pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages")))))
.pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages"))))) //
.as("matches");
assertThat(lookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
@@ -216,4 +217,54 @@ public class LookupOperationUnitTests {
}}
""");
}
@Test // GH-4379
void unmappedLookupWithFromExtractedFromType() {
LookupOperation lookupOperation = Aggregation.lookup().from(Restaurant.class) //
.localField("restaurant_name") //
.foreignField("name") //
.as("restaurants");
assertThat(lookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
{ $lookup:
{
from: "restaurant",
localField: "restaurant_name",
foreignField: "name",
as: "restaurants"
}
}}
""");
}
@Test // GH-4379
void mappedLookupWithFromExtractedFromType() {
LookupOperation lookupOperation = Aggregation.lookup().from(Restaurant.class) //
.localField("restaurant_name") //
.foreignField("name") //
.as("restaurants");
assertThat(lookupOperation.toDocument(AggregationTestUtils.strict(Restaurant.class).ctx())).isEqualTo("""
{ $lookup:
{
from: "sites",
localField: "restaurant_name",
foreignField: "rs_name",
as: "restaurants"
}
}}
""");
}
@org.springframework.data.mongodb.core.mapping.Document("sites")
static class Restaurant {
String id;
@Field("rs_name") //
String name;
}
}

View File

@@ -22,6 +22,7 @@ import static org.springframework.data.mongodb.core.query.Query.*;
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
@@ -97,6 +98,18 @@ class QueryTests {
assertThat(q.getQueryObject()).isEqualTo(Document
.parse("{ \"name\" : { \"$gte\" : \"M\" , \"$lte\" : \"T\"} , \"age\" : { \"$not\" : { \"$gt\" : 22}}}"));
assertThat(q.getLimit()).isEqualTo(50);
q.limit(Limit.unlimited());
assertThat(q.getLimit()).isZero();
assertThat(q.isLimited()).isFalse();
q.limit(Limit.of(10));
assertThat(q.getLimit()).isEqualTo(10);
assertThat(q.isLimited()).isTrue();
q.limit(Limit.of(-1));
assertThat(q.getLimit()).isZero();
assertThat(q.isLimited()).isFalse();
}
@Test

View File

@@ -213,6 +213,17 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
assertThat(page).contains(carter);
}
@Test // GH-4397
void appliesLimitToScrollingCorrectly() {
Window<Person> page = repository.findByLastnameLikeOrderByLastnameAscFirstnameAsc("*a*",
ScrollPosition.keyset(), Limit.of(2));
assertThat(page.isLast()).isFalse();
assertThat(page.size()).isEqualTo(2);
assertThat(page).contains(carter);
}
@Test // GH-4308
void appliesScrollPositionWithProjectionCorrectly() {
@@ -236,6 +247,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie
assertThat(page).contains(carter, stefan);
}
@Test // GH-4397
void executesFinderCorrectlyWithSortAndLimit() {
List<Person> page = repository.findByLastnameLike("*a*", Sort.by(Direction.ASC, "lastname", "firstname"), Limit.of(2));
assertThat(page).containsExactly(carter, stefan);
}
@Test
void executesPagedFinderWithAnnotatedQueryCorrectly() {

View File

@@ -23,6 +23,7 @@ import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
@@ -126,6 +127,9 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
Window<Person> findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
ScrollPosition scrollPosition);
Window<Person> findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname,
ScrollPosition scrollPosition, Limit limit);
/**
* Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards
* supported).
@@ -145,6 +149,8 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
*/
Page<Person> findByLastnameLike(String lastname, Pageable pageable);
List<Person> findByLastnameLike(String lastname, Sort sort, Limit limit);
@Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}")
Page<Person> findByLastnameLikeWithPageable(String lastname, Pageable pageable);

View File

@@ -36,6 +36,7 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -493,6 +494,30 @@ class AbstractMongoQueryUnitTests {
assertThat(captor.getValue().getHint()).isEqualTo("idx-ln");
}
@Test // GH-4397
void limitShouldBeAppliedToQuery() {
createQueryForMethod("findWithLimit", String.class, Limit.class).execute(new Object[] { "dalinar", Limit.of(42) });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
assertThat(captor.getValue().getLimit()).isEqualTo(42);
}
@Test // GH-4397
void sortAndLimitShouldBeAppliedToQuery() {
createQueryForMethod("findWithSortAndLimit", String.class, Sort.class, Limit.class)
.execute(new Object[] { "dalinar", Sort.by("fn"), Limit.of(42) });
ArgumentCaptor<Query> captor = ArgumentCaptor.forClass(Query.class);
verify(withQueryMock).matching(captor.capture());
assertThat(captor.getValue().getLimit()).isEqualTo(42);
assertThat(captor.getValue().getSortObject()).isEqualTo(new Document("fn", 1));
}
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
return createQueryForMethod(Repo.class, methodName, paramTypes);
}
@@ -614,6 +639,10 @@ class AbstractMongoQueryUnitTests {
@Hint("idx-fn")
void findWithHintByFirstname(String firstname);
List<Person> findWithLimit(String firstname, Limit limit);
List<Person> findWithSortAndLimit(String firstname, Sort sort, Limit limit);
}
// DATAMONGO-1872