stream();
/**
- * Get the number of matching elements.
- *
- * This method uses an {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) aggregation
- * execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees shard,
- * session and transaction compliance. In case an inaccurate count satisfies the applications needs use
- * {@link MongoOperations#estimatedCount(String)} for empty queries instead.
+ * Return a window of elements either starting or resuming at
+ * {@link org.springframework.data.domain.ScrollPosition}.
+ *
+ * When using {@link KeysetScrollPosition}, make sure to use non-nullable
+ * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct
+ * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators.
+ *
+ * @param scrollPosition the scroll position.
+ * @return a window of the resulting elements.
+ * @since 4.1
+ * @see org.springframework.data.domain.OffsetScrollPosition
+ * @see org.springframework.data.domain.KeysetScrollPosition
+ */
+ Window scroll(ScrollPosition scrollPosition);
+
+ /**
+ * Get the number of matching elements.
+ * This method uses an
+ * {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
+ * aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but
+ * guarantees shard, session and transaction compliance. In case an inaccurate count satisfies the applications
+ * needs use {@link MongoOperations#estimatedCount(String)} for empty queries instead.
*
* @return total number of matching elements.
*/
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
index 1038ee2327..d99cffbe37 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
@@ -21,6 +21,8 @@
import org.bson.Document;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.data.domain.Window;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.SerializationUtils;
@@ -71,8 +73,8 @@ static class ExecutableFindSupport
private final @Nullable String collection;
private final Query query;
- ExecutableFindSupport(MongoTemplate template, Class> domainType, Class returnType,
- @Nullable String collection, Query query) {
+ ExecutableFindSupport(MongoTemplate template, Class> domainType, Class returnType, @Nullable String collection,
+ Query query) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
@@ -138,6 +140,11 @@ public Stream stream() {
return doStream();
}
+ @Override
+ public Window scroll(ScrollPosition scrollPosition) {
+ return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName());
+ }
+
@Override
public TerminatingFindNear near(NearQuery nearQuery) {
return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType);
@@ -168,8 +175,7 @@ private List doFind(@Nullable CursorPreparer preparer) {
Document fieldsObject = query.getFieldsObject();
return template.doFind(template.createDelegate(query), getCollectionName(), queryObject, fieldsObject, domainType,
- returnType,
- getCursorPreparer(query, preparer));
+ returnType, getCursorPreparer(query, preparer));
}
private List doFindDistinct(String field) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
index 4727c0b8db..2f3c0dd926 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
@@ -23,6 +23,8 @@
import java.util.stream.Stream;
import org.bson.Document;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.Window;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -319,7 +321,8 @@ default MongoCollection createView(String name, Class> source, Aggre
* @param options additional settings to apply when creating the view. Can be {@literal null}.
* @since 4.0
*/
- MongoCollection createView(String name, Class> source, AggregationPipeline pipeline, @Nullable ViewOptions options);
+ MongoCollection createView(String name, Class> source, AggregationPipeline pipeline,
+ @Nullable ViewOptions options);
/**
* Create a view with the provided name. The view content is defined by the {@link AggregationPipeline pipeline} on
@@ -331,7 +334,8 @@ default MongoCollection createView(String name, Class> source, Aggre
* @param options additional settings to apply when creating the view. Can be {@literal null}.
* @since 4.0
*/
- MongoCollection createView(String name, String source, AggregationPipeline pipeline, @Nullable ViewOptions options);
+ MongoCollection createView(String name, String source, AggregationPipeline pipeline,
+ @Nullable ViewOptions options);
/**
* A set of collection names.
@@ -802,6 +806,57 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin
*/
List find(Query query, Class entityClass, String collectionName);
+ /**
+ * Query for a window of objects of type T from the specified collection.
+ * Make sure to either set {@link Query#skip(long)} or {@link Query#with(KeysetScrollPosition)} along with
+ * {@link Query#limit(int)} to limit large query results for efficient scrolling.
+ * Result objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}.
+ * Unless configured otherwise, an instance of {@link MappingMongoConverter} will be used.
+ * If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
+ * to map objects since the test for class type is done in the client and not on the server.
+ *
+ * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
+ * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
+ * {@code null} values through {@code $gt/$lt} operators.
+ *
+ * @param query the query class that specifies the criteria used to find a record and also an optional fields
+ * specification. Must not be {@literal null}.
+ * @param entityType the parametrized type of the returned window.
+ * @return the converted window.
+ * @throws IllegalStateException if a potential {@link Query#getKeyset() KeysetScrollPosition} contains an invalid
+ * position.
+ * @since 4.1
+ * @see Query#with(org.springframework.data.domain.OffsetScrollPosition)
+ * @see Query#with(org.springframework.data.domain.KeysetScrollPosition)
+ */
+ Window scroll(Query query, Class entityType);
+
+ /**
+ * Query for a window of objects of type T from the specified collection.
+ * Make sure to either set {@link Query#skip(long)} or {@link Query#with(KeysetScrollPosition)} along with
+ * {@link Query#limit(int)} to limit large query results for efficient scrolling.
+ * Result objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}.
+ * Unless configured otherwise, an instance of {@link MappingMongoConverter} will be used.
+ * If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
+ * to map objects since the test for class type is done in the client and not on the server.
+ *
+ * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
+ * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
+ * {@code null} values through {@code $gt/$lt} operators.
+ *
+ * @param query the query class that specifies the criteria used to find a record and also an optional fields
+ * specification. Must not be {@literal null}.
+ * @param entityType the parametrized type of the returned window.
+ * @param collectionName name of the collection to retrieve the objects from.
+ * @return the converted window.
+ * @throws IllegalStateException if a potential {@link Query#getKeyset() KeysetScrollPosition} contains an invalid
+ * position.
+ * @since 4.1
+ * @see Query#with(org.springframework.data.domain.OffsetScrollPosition)
+ * @see Query#with(org.springframework.data.domain.KeysetScrollPosition)
+ */
+ Window scroll(Query query, Class entityType, String collectionName);
+
/**
* Returns a document with the given id mapped onto the given class. The collection the query is ran against will be
* derived from the given target class as well.
@@ -1175,7 +1230,7 @@ T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
* @param entityClass class that determines the collection to use. Must not be {@literal null}.
* @return the count of matching documents.
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
- * {@link #getCollectionName(Class) derived} from the given type.
+ * {@link #getCollectionName(Class) derived} from the given type.
* @see #exactCount(Query, Class)
* @see #estimatedCount(Class)
*/
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 c23548ea3a..00e8132de5 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
@@ -44,6 +44,8 @@
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.convert.EntityReader;
+import org.springframework.data.domain.OffsetScrollPosition;
+import org.springframework.data.domain.Window;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
@@ -64,6 +66,7 @@
import org.springframework.data.mongodb.core.QueryOperations.DistinctQueryContext;
import org.springframework.data.mongodb.core.QueryOperations.QueryContext;
import org.springframework.data.mongodb.core.QueryOperations.UpdateContext;
+import org.springframework.data.mongodb.core.ScrollUtils.KeySetScrollQuery;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
@@ -847,6 +850,49 @@ public List find(Query query, Class entityClass, String collectionName
new QueryCursorPreparer(query, entityClass));
}
+ @Override
+ public Window scroll(Query query, Class entityType) {
+
+ Assert.notNull(entityType, "Entity type must not be null");
+
+ return scroll(query, entityType, getCollectionName(entityType));
+ }
+
+ @Override
+ public Window scroll(Query query, Class entityType, String collectionName) {
+ return doScroll(query, entityType, entityType, collectionName);
+ }
+
+ Window doScroll(Query query, Class> sourceClass, Class targetClass, String collectionName) {
+
+ Assert.notNull(query, "Query must not be null");
+ Assert.notNull(collectionName, "CollectionName must not be null");
+ Assert.notNull(sourceClass, "Entity type must not be null");
+ Assert.notNull(targetClass, "Target type must not be null");
+
+ EntityProjection projection = operations.introspectProjection(targetClass, sourceClass);
+ ProjectingReadCallback,T> callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
+ int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE;
+
+ if (query.hasKeyset()) {
+
+ KeySetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query,
+ operations.getIdPropertyName(sourceClass));
+
+ List result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(),
+ keysetPaginationQuery.fields(), sourceClass,
+ new QueryCursorPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback);
+
+ return ScrollUtils.createWindow(query.getSortObject(), query.getLimit(), result, sourceClass, operations);
+ }
+
+ List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(),
+ sourceClass, new QueryCursorPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass),
+ callback);
+
+ return ScrollUtils.createWindow(result, query.getLimit(), OffsetScrollPosition.positionFunction(query.getSkip()));
+ }
+
@Nullable
@Override
public T findById(Object id, Class entityClass) {
@@ -953,7 +999,7 @@ public GeoResults geoNear(NearQuery near, Class> domainType, String col
optionsBuilder.readPreference(near.getReadPreference());
}
- if(near.hasReadConcern()) {
+ if (near.hasReadConcern()) {
optionsBuilder.readConcern(near.getReadConcern());
}
@@ -2837,13 +2883,24 @@ private static MongoConverter getDefaultMongoConverter(MongoDatabaseFactory fact
return converter;
}
- private Document getMappedSortObject(Query query, Class> type) {
+ @Nullable
+ private Document getMappedSortObject(@Nullable Query query, Class> type) {
- if (query == null || ObjectUtils.isEmpty(query.getSortObject())) {
+ if (query == null) {
return null;
}
- return queryMapper.getMappedSort(query.getSortObject(), mappingContext.getPersistentEntity(type));
+ return getMappedSortObject(query.getSortObject(), type);
+ }
+
+ @Nullable
+ private Document getMappedSortObject(Document sortObject, Class> type) {
+
+ if (ObjectUtils.isEmpty(sortObject)) {
+ return null;
+ }
+
+ return queryMapper.getMappedSort(sortObject, mappingContext.getPersistentEntity(type));
}
/**
@@ -3206,11 +3263,23 @@ public T doWith(Document document) {
class QueryCursorPreparer implements CursorPreparer {
private final Query query;
+
+ private final Document sortObject;
+
+ private final int limit;
+
+ private final long skip;
private final @Nullable Class> type;
QueryCursorPreparer(Query query, @Nullable Class> type) {
+ this(query, query.getSortObject(), query.getLimit(), query.getSkip(), type);
+ }
+ QueryCursorPreparer(Query query, Document sortObject, int limit, long skip, @Nullable Class> type) {
this.query = query;
+ this.sortObject = sortObject;
+ this.limit = limit;
+ this.skip = skip;
this.type = type;
}
@@ -3225,20 +3294,20 @@ public FindIterable prepare(FindIterable iterable) {
Meta meta = query.getMeta();
HintFunction hintFunction = HintFunction.from(query.getHint());
- if (query.getSkip() <= 0 && query.getLimit() <= 0 && ObjectUtils.isEmpty(query.getSortObject())
- && hintFunction.isEmpty() && !meta.hasValues() && query.getCollation().isEmpty()) {
+ if (skip <= 0 && limit <= 0 && ObjectUtils.isEmpty(sortObject) && hintFunction.isEmpty() && !meta.hasValues()
+ && query.getCollation().isEmpty()) {
return cursorToUse;
}
try {
- if (query.getSkip() > 0) {
- cursorToUse = cursorToUse.skip((int) query.getSkip());
+ if (skip > 0) {
+ cursorToUse = cursorToUse.skip((int) skip);
}
- if (query.getLimit() > 0) {
- cursorToUse = cursorToUse.limit(query.getLimit());
+ if (limit > 0) {
+ cursorToUse = cursorToUse.limit(limit);
}
- if (!ObjectUtils.isEmpty(query.getSortObject())) {
- Document sort = type != null ? getMappedSortObject(query, type) : query.getSortObject();
+ if (!ObjectUtils.isEmpty(sortObject)) {
+ Document sort = type != null ? getMappedSortObject(sortObject, type) : sortObject;
cursorToUse = cursorToUse.sort(sort);
}
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 05aeda069b..4e8c5f63de 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
@@ -28,6 +28,7 @@
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.codecs.Codec;
+import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
@@ -776,7 +777,7 @@ Document applyShardKey(MongoPersistentEntity domainType, Document filter,
Document filterWithShardKey = new Document(filter);
getMappedShardKeyFields(domainType)
- .forEach(key -> filterWithShardKey.putIfAbsent(key, BsonUtils.resolveValue(shardKeySource, key)));
+ .forEach(key -> filterWithShardKey.putIfAbsent(key, BsonUtils.resolveValue((Bson) shardKeySource, key)));
return filterWithShardKey;
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
index 31de934ec7..4456ab6ac4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
@@ -18,6 +18,9 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Window;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.data.mongodb.core.query.NearQuery;
@@ -87,14 +90,27 @@ interface TerminatingFind {
*/
Flux all();
+ /**
+ * Return a scroll of elements either starting or resuming at {@link ScrollPosition}.
+ *
+ * When using {@link KeysetScrollPosition}, make sure to use non-nullable
+ * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct
+ * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators.
+ *
+ * @param scrollPosition the scroll position.
+ * @return a scroll of the resulting elements.
+ * @since 4.1
+ * @see org.springframework.data.domain.OffsetScrollPosition
+ * @see org.springframework.data.domain.KeysetScrollPosition
+ */
+ Mono> scroll(ScrollPosition scrollPosition);
+
/**
* Get all matching elements using a {@link com.mongodb.CursorType#TailableAwait tailable cursor}. The stream will
* not be completed unless the {@link org.reactivestreams.Subscription} is
- * {@link org.reactivestreams.Subscription#cancel() canceled}.
- *
+ * {@link org.reactivestreams.Subscription#cancel() canceled}.
* However, the stream may become dead, or invalid, if either the query returns no match or the cursor returns the
- * document at the "end" of the collection and then the application deletes that document.
- *
+ * document at the "end" of the collection and then the application deletes that document.
* A stream that is no longer in use must be {@link reactor.core.Disposable#dispose()} disposed} otherwise the
* streams will linger and exhaust resources.
* NOTE: Requires a capped collection.
@@ -105,8 +121,7 @@ interface TerminatingFind {
Flux tail();
/**
- * Get the number of matching elements.
- *
+ * Get the number of matching elements.
* This method uses an
* {@link com.mongodb.reactivestreams.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java
index 4dcf62aacd..30b8ab0921 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java
@@ -20,6 +20,8 @@
import org.bson.Document;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.data.domain.Window;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.mongodb.core.CollectionPreparerSupport.ReactiveCollectionPreparerDelegate;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
@@ -137,6 +139,11 @@ public Flux all() {
return doFind(null);
}
+ @Override
+ public Mono> scroll(ScrollPosition scrollPosition) {
+ return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName());
+ }
+
@Override
public Flux tail() {
return doFind(template.new TailingQueryFindPublisherPreparer(query, domainType));
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
index 323ca9dd95..af36989654 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
@@ -25,7 +25,8 @@
import org.bson.Document;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
-
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.Window;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -279,7 +280,8 @@ default Mono> createView(String name, Class> source,
* @param options additional settings to apply when creating the view. Can be {@literal null}.
* @since 4.0
*/
- Mono> createView(String name, Class> source, AggregationPipeline pipeline, @Nullable ViewOptions options);
+ Mono> createView(String name, Class> source, AggregationPipeline pipeline,
+ @Nullable ViewOptions options);
/**
* Create a view with the provided name. The view content is defined by the {@link AggregationPipeline pipeline} on
@@ -291,7 +293,8 @@ default Mono> createView(String name, Class> source,
* @param options additional settings to apply when creating the view. Can be {@literal null}.
* @since 4.0
*/
- Mono> createView(String name, String source, AggregationPipeline pipeline, @Nullable ViewOptions options);
+ Mono> createView(String name, String source, AggregationPipeline pipeline,
+ @Nullable ViewOptions options);
/**
* A set of collection names.
@@ -462,6 +465,57 @@ default Mono> createView(String name, Class> source,
*/
Flux find(Query query, Class entityClass, String collectionName);
+ /**
+ * Query for a scroll of objects of type T from the specified collection.
+ * Make sure to either set {@link Query#skip(long)} or {@link Query#with(KeysetScrollPosition)} along with
+ * {@link Query#limit(int)} to limit large query results for efficient scrolling.
+ * Result objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}.
+ * Unless configured otherwise, an instance of {@link MappingMongoConverter} will be used.
+ * If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
+ * to map objects since the test for class type is done in the client and not on the server.
+ *
+ * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
+ * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
+ * {@code null} values through {@code $gt/$lt} operators.
+ *
+ * @param query the query class that specifies the criteria used to find a record and also an optional fields
+ * specification. Must not be {@literal null}.
+ * @param entityType the parametrized type of the returned list.
+ * @return {@link Mono} emitting the converted window.
+ * @throws IllegalStateException if a potential {@link Query#getKeyset() KeysetScrollPosition} contains an invalid
+ * position.
+ * @since 4.1
+ * @see Query#with(org.springframework.data.domain.OffsetScrollPosition)
+ * @see Query#with(org.springframework.data.domain.KeysetScrollPosition)
+ */
+ Mono> scroll(Query query, Class entityType);
+
+ /**
+ * Query for a window of objects of type T from the specified collection.
+ * Make sure to either set {@link Query#skip(long)} or {@link Query#with(KeysetScrollPosition)} along with
+ * {@link Query#limit(int)} to limit large query results for efficient scrolling.
+ * Result objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}.
+ * Unless configured otherwise, an instance of {@link MappingMongoConverter} will be used.
+ * If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
+ * to map objects since the test for class type is done in the client and not on the server.
+ *
+ * When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort
+ * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or
+ * {@code null} values through {@code $gt/$lt} operators.
+ *
+ * @param query the query class that specifies the criteria used to find a record and also an optional fields
+ * specification. Must not be {@literal null}.
+ * @param entityType the parametrized type of the returned list.
+ * @param collectionName name of the collection to retrieve the objects from.
+ * @return {@link Mono} emitting the converted window.
+ * @throws IllegalStateException if a potential {@link Query#getKeyset() KeysetScrollPosition} contains an invalid
+ * position.
+ * @since 4.1
+ * @see Query#with(org.springframework.data.domain.OffsetScrollPosition)
+ * @see Query#with(org.springframework.data.domain.KeysetScrollPosition)
+ */
+ Mono> scroll(Query query, Class entityType, String collectionName);
+
/**
* Returns a document with the given id mapped onto the given class. The collection the query is ran against will be
* derived from the given target class as well.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
index 3983ee1e0c..1e99adddbe 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
@@ -58,6 +58,8 @@
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.convert.EntityReader;
+import org.springframework.data.domain.OffsetScrollPosition;
+import org.springframework.data.domain.Window;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.Metric;
@@ -78,6 +80,7 @@
import org.springframework.data.mongodb.core.QueryOperations.DistinctQueryContext;
import org.springframework.data.mongodb.core.QueryOperations.QueryContext;
import org.springframework.data.mongodb.core.QueryOperations.UpdateContext;
+import org.springframework.data.mongodb.core.ScrollUtils.KeySetScrollQuery;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
@@ -826,6 +829,51 @@ public Flux find(@Nullable Query query, Class entityClass, String coll
query.getFieldsObject(), entityClass, new QueryFindPublisherPreparer(query, entityClass));
}
+ @Override
+ public Mono> scroll(Query query, Class entityType) {
+
+ Assert.notNull(entityType, "Entity type must not be null");
+
+ return scroll(query, entityType, getCollectionName(entityType));
+ }
+
+ @Override
+ public Mono> scroll(Query query, Class entityType, String collectionName) {
+ return doScroll(query, entityType, entityType, collectionName);
+ }
+
+ Mono> doScroll(Query query, Class> sourceClass, Class targetClass, String collectionName) {
+
+ Assert.notNull(query, "Query must not be null");
+ Assert.notNull(collectionName, "CollectionName must not be null");
+ Assert.notNull(sourceClass, "Entity type must not be null");
+ Assert.notNull(targetClass, "Target type must not be null");
+
+ EntityProjection projection = operations.introspectProjection(targetClass, sourceClass);
+ ProjectingReadCallback,T> callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName);
+ int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE;
+
+ if (query.hasKeyset()) {
+
+ KeySetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query,
+ operations.getIdPropertyName(sourceClass));
+
+ Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query),
+ keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass,
+ new QueryFindPublisherPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback).collectList();
+
+ return result.map(it -> ScrollUtils.createWindow(query.getSortObject(), query.getLimit(), it, sourceClass, operations));
+ }
+
+ Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(),
+ query.getFieldsObject(), sourceClass,
+ new QueryFindPublisherPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback)
+ .collectList();
+
+ return result.map(
+ it -> ScrollUtils.createWindow(it, query.getLimit(), OffsetScrollPosition.positionFunction(query.getSkip())));
+ }
+
@Override
public Mono findById(Object id, Class entityClass) {
return findById(id, entityClass, getCollectionName(entityClass));
@@ -1004,7 +1052,7 @@ protected Flux> geoNear(NearQuery near, Class> entityClass, S
optionsBuilder.readPreference(near.getReadPreference());
}
- if(near.hasReadConcern()) {
+ if (near.hasReadConcern()) {
optionsBuilder.readConcern(near.getReadConcern());
}
@@ -2652,13 +2700,24 @@ private MappingMongoConverter getDefaultMongoConverter() {
return converter;
}
+ @Nullable
private Document getMappedSortObject(Query query, Class> type) {
if (query == null) {
return null;
}
- return queryMapper.getMappedSort(query.getSortObject(), mappingContext.getPersistentEntity(type));
+ return getMappedSortObject(query.getSortObject(), type);
+ }
+
+ @Nullable
+ private Document getMappedSortObject(Document sortObject, Class> type) {
+
+ if (ObjectUtils.isEmpty(sortObject)) {
+ return null;
+ }
+
+ return queryMapper.getMappedSort(sortObject, mappingContext.getPersistentEntity(type));
}
// Callback implementations
@@ -3088,11 +3147,24 @@ public Mono> doWith(Document object) {
class QueryFindPublisherPreparer implements FindPublisherPreparer {
private final Query query;
+
+ private final Document sortObject;
+
+ private final int limit;
+
+ private final long skip;
private final @Nullable Class> type;
QueryFindPublisherPreparer(Query query, @Nullable Class> type) {
+ this(query, query.getSortObject(), query.getLimit(), query.getSkip(), type);
+ }
+
+ QueryFindPublisherPreparer(Query query, Document sortObject, int limit, long skip, @Nullable Class> type) {
this.query = query;
+ this.sortObject = sortObject;
+ this.limit = limit;
+ this.skip = skip;
this.type = type;
}
@@ -3107,23 +3179,23 @@ public FindPublisher prepare(FindPublisher findPublisher) {
HintFunction hintFunction = HintFunction.from(query.getHint());
Meta meta = query.getMeta();
- if (query.getSkip() <= 0 && query.getLimit() <= 0 && ObjectUtils.isEmpty(query.getSortObject())
- && hintFunction.isEmpty() && !meta.hasValues()) {
+ if (skip <= 0 && limit <= 0 && ObjectUtils.isEmpty(sortObject) && hintFunction.isEmpty()
+ && !meta.hasValues()) {
return findPublisherToUse;
}
try {
- if (query.getSkip() > 0) {
- findPublisherToUse = findPublisherToUse.skip((int) query.getSkip());
+ if (skip > 0) {
+ findPublisherToUse = findPublisherToUse.skip((int) skip);
}
- if (query.getLimit() > 0) {
- findPublisherToUse = findPublisherToUse.limit(query.getLimit());
+ if (limit > 0) {
+ findPublisherToUse = findPublisherToUse.limit(limit);
}
- if (!ObjectUtils.isEmpty(query.getSortObject())) {
- Document sort = type != null ? getMappedSortObject(query, type) : query.getSortObject();
+ if (!ObjectUtils.isEmpty(sortObject)) {
+ Document sort = type != null ? getMappedSortObject(sortObject, type) : sortObject;
findPublisherToUse = findPublisherToUse.sort(sort);
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java
new file mode 100644
index 0000000000..112f95270b
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java
@@ -0,0 +1,160 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.IntFunction;
+
+import org.bson.BsonNull;
+import org.bson.Document;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.KeysetScrollPosition.Direction;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Window;
+import org.springframework.data.mongodb.core.EntityOperations.Entity;
+import org.springframework.data.mongodb.core.query.Query;
+
+/**
+ * Utilities to run scroll queries and create {@link Window} results.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @since 4.1
+ */
+class ScrollUtils {
+
+ /**
+ * Create the actual query to run keyset-based pagination. Affects projection, sorting, and the criteria.
+ *
+ * @param query
+ * @param idPropertyName
+ * @return
+ */
+ static KeySetScrollQuery createKeysetPaginationQuery(Query query, String idPropertyName) {
+
+ KeysetScrollPosition keyset = query.getKeyset();
+ Map keysetValues = keyset.getKeys();
+ Document queryObject = query.getQueryObject();
+
+ Document sortObject = query.isSorted() ? query.getSortObject() : new Document();
+ sortObject.put(idPropertyName, 1);
+
+ // make sure we can extract the keyset
+ Document fieldsObject = query.getFieldsObject();
+ if (!fieldsObject.isEmpty()) {
+ for (String field : sortObject.keySet()) {
+ fieldsObject.put(field, 1);
+ }
+ }
+
+ List or = (List) queryObject.getOrDefault("$or", new ArrayList<>());
+ List sortKeys = new ArrayList<>(sortObject.keySet());
+
+ if (!keysetValues.isEmpty() && !keysetValues.keySet().containsAll(sortKeys)) {
+ throw new IllegalStateException("KeysetScrollPosition does not contain all keyset values");
+ }
+
+ // first query doesn't come with a keyset
+ if (!keysetValues.isEmpty()) {
+
+ // build matrix query for keyset paging that contains sort^2 queries
+ // reflecting a query that follows sort order semantics starting from the last returned keyset
+ for (int i = 0; i < sortKeys.size(); i++) {
+
+ Document sortConstraint = new Document();
+
+ for (int j = 0; j < sortKeys.size(); j++) {
+
+ String sortSegment = sortKeys.get(j);
+ int sortOrder = sortObject.getInteger(sortSegment);
+ Object o = keysetValues.get(sortSegment);
+
+ if (j >= i) { // tail segment
+ if (o instanceof BsonNull) {
+ throw new IllegalStateException(
+ "Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(sortSegment));
+ }
+ sortConstraint.put(sortSegment, new Document(getComparator(sortOrder, keyset.getDirection()), o));
+ break;
+ }
+
+ sortConstraint.put(sortSegment, o);
+ }
+
+ if (!sortConstraint.isEmpty()) {
+ or.add(sortConstraint);
+ }
+ }
+ }
+
+ if (!or.isEmpty()) {
+ queryObject.put("$or", or);
+ }
+
+ return new KeySetScrollQuery(queryObject, fieldsObject, sortObject);
+ }
+
+ private static String getComparator(int sortOrder, Direction direction) {
+
+ // use gte/lte to include the object at the cursor/keyset so that
+ // we can include it in the result to check whether there is a next object.
+ // It needs to be filtered out later on.
+ if (direction == Direction.Backward) {
+ return sortOrder == 0 ? "$gte" : "$lte";
+ }
+
+ return sortOrder == 1 ? "$gt" : "$lt";
+ }
+
+ static Window createWindow(Document sortObject, int limit, List result, Class> sourceType,
+ EntityOperations operations) {
+
+ IntFunction positionFunction = value -> {
+
+ T last = result.get(value);
+ Entity entity = operations.forEntity(last);
+
+ Map keys = entity.extractKeys(sortObject, sourceType);
+ return KeysetScrollPosition.of(keys);
+ };
+
+ return createWindow(result, limit, positionFunction);
+ }
+
+ static Window createWindow(List result, int limit, IntFunction extends ScrollPosition> positionFunction) {
+ return Window.from(getSubList(result, limit), positionFunction, hasMoreElements(result, limit));
+ }
+
+ static boolean hasMoreElements(List> result, int limit) {
+ return !result.isEmpty() && result.size() > limit;
+ }
+
+ static List getSubList(List result, int limit) {
+
+ if (limit > 0 && result.size() > limit) {
+ return result.subList(0, limit);
+ }
+
+ return result;
+ }
+
+ record KeySetScrollQuery(Document query, Document fields, Document sort) {
+
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java
index 912e7d5cea..e631852a42 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java
@@ -30,7 +30,10 @@
import java.util.Set;
import org.bson.Document;
+import org.springframework.data.domain.KeysetScrollPosition;
+import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
@@ -64,6 +67,8 @@ public class Query implements ReadConcernAware, ReadPreferenceAware {
private Sort sort = Sort.unsorted();
private long skip;
private int limit;
+
+ private KeysetScrollPosition keysetScrollPosition;
private @Nullable ReadConcern readConcern;
private @Nullable ReadPreference readPreference;
@@ -255,6 +260,67 @@ public Query with(Pageable pageable) {
return with(pageable.getSort());
}
+ /**
+ * Sets the given cursor position on the {@link Query} instance. Will transparently set {@code skip}.
+ *
+ * @param position must not be {@literal null}.
+ * @return this.
+ */
+ public Query with(ScrollPosition position) {
+
+ Assert.notNull(position, "ScrollPosition must not be null");
+
+ if (position instanceof OffsetScrollPosition offset) {
+ return with(offset);
+ }
+
+ if (position instanceof KeysetScrollPosition keyset) {
+ return with(keyset);
+ }
+
+ throw new IllegalArgumentException(String.format("ScrollPosition %s not supported", position));
+ }
+
+ /**
+ * Sets the given cursor position on the {@link Query} instance. Will transparently set {@code skip}.
+ *
+ * @param position must not be {@literal null}.
+ * @return this.
+ */
+ public Query with(OffsetScrollPosition position) {
+
+ Assert.notNull(position, "ScrollPosition must not be null");
+
+ this.skip = position.getOffset();
+ this.keysetScrollPosition = null;
+ return this;
+ }
+
+ /**
+ * Sets the given cursor position on the {@link Query} instance. Will transparently reset {@code skip}.
+ *
+ * @param position must not be {@literal null}.
+ * @return this.
+ */
+ public Query with(KeysetScrollPosition position) {
+
+ Assert.notNull(position, "ScrollPosition must not be null");
+
+ this.skip = 0;
+ this.keysetScrollPosition = position;
+
+ return this;
+ }
+
+ public boolean hasKeyset() {
+ return keysetScrollPosition != null;
+ }
+
+ @Nullable
+ public KeysetScrollPosition getKeyset() {
+ return keysetScrollPosition;
+ }
+
/**
* Adds a {@link Sort} to the {@link Query} instance.
*
@@ -384,11 +450,22 @@ public long getSkip() {
return this.skip;
}
+ /**
+ * Returns whether the query is {@link #limit(int) limited}.
+ *
+ * @return {@code true} if the query is limited; {@code false} otherwise.
+ * @since 4.1
+ */
+ public boolean isLimited() {
+ return this.limit > 0;
+ }
+
/**
* Get the maximum number of documents to be return. {@literal Zero} or a {@literal negative} value indicates no
* limit.
*
* @return number of documents to return.
+ * @see #isLimited()
*/
public int getLimit() {
return this.limit;
@@ -688,4 +765,5 @@ public int hashCode() {
public static boolean isRestrictedTypeKey(String key) {
return RESTRICTED_TYPES_KEY.equals(key);
}
+
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
index 823b64d324..930a733315 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
@@ -167,6 +167,9 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
return q -> operation.matching(q).stream();
} else if (method.isCollectionQuery()) {
return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort())).all();
+ } else if (method.isScrollQuery()) {
+ return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort()))
+ .scroll(accessor.getScrollPosition());
} else if (method.isPageQuery()) {
return new PagedExecution(operation, accessor.getPageable());
} else if (isCountQuery()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
index ed1fee68c6..fbb078b43e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java
@@ -203,6 +203,9 @@ private ReactiveMongoQueryExecution getExecutionToWrap(MongoParameterAccessor ac
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).tail();
} else if (method.isCollectionQuery()) {
return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all();
+ } else if (method.isScrollQuery()) {
+ return (q, t, c) -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort()))
+ .scroll(accessor.getScrollPosition());
} else if (isCountQuery()) {
return (q, t, c) -> operation.matching(q).count();
} else if (isExistsQuery()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
index 21513efbfb..b3ecef9856 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java
@@ -23,6 +23,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Range;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
@@ -71,6 +72,11 @@ public PotentiallyConvertingIterator iterator() {
return new ConvertingIterator(delegate.iterator());
}
+ @Override
+ public ScrollPosition getScrollPosition() {
+ return delegate.getScrollPosition();
+ }
+
public Pageable getPageable() {
return delegate.getPageable();
}
@@ -197,7 +203,7 @@ private static Collection> asCollection(@Nullable Object source) {
if (source instanceof Iterable) {
- if(source instanceof Collection) {
+ if (source instanceof Collection) {
return new ArrayList<>((Collection>) source);
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
index 451e743389..1b8f6b6a58 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java
@@ -335,8 +335,8 @@ public boolean hasAnnotatedCollation() {
public String getAnnotatedCollation() {
return doFindAnnotation(Collation.class).map(Collation::value) //
- .orElseThrow(() -> new IllegalStateException(
- "Expected to find @Collation annotation but did not; Make sure to check hasAnnotatedCollation() before."));
+ .orElseThrow(() -> new IllegalStateException(
+ "Expected to find @Collation annotation but did not; Make sure to check hasAnnotatedCollation() before."));
}
/**
@@ -420,7 +420,8 @@ public void verify() {
if (isModifyingQuery()) {
- if (isCollectionQuery() || isSliceQuery() || isPageQuery() || isGeoNearQuery() || !isNumericOrVoidReturnValue()) { //
+ if (isCollectionQuery() || isScrollQuery() || isSliceQuery() || isPageQuery() || isGeoNearQuery()
+ || !isNumericOrVoidReturnValue()) { //
throw new IllegalStateException(
String.format("Update method may be void or return a numeric value (the number of updated documents)."
+ "Offending method: %s", method));
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java
index a64822a263..904c184a8f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethod.java
@@ -66,7 +66,7 @@ public ReactiveMongoQueryMethod(Method method, RepositoryMetadata metadata, Proj
super(method, metadata, projectionFactory, mappingContext);
this.method = method;
- this.isCollectionQuery = Lazy.of(() -> (!(isPageQuery() || isSliceQuery())
+ this.isCollectionQuery = Lazy.of(() -> (!(isPageQuery() || isSliceQuery() || isScrollQuery())
&& ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType()) || super.isCollectionQuery()));
}
@@ -136,7 +136,16 @@ public void verify() {
boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType());
boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType())
&& (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())
- || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()));
+ || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()));
+
+ if (hasParameterOfType(method, Sort.class)) {
+ throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter;"
+ + " Use sorting capabilities on Pageable instead; Offending method: %s", method));
+ }
+
+ if (isScrollQuery()) {
+ return;
+ }
if (singleWrapperWithWrappedPageableResult) {
throw new InvalidDataAccessApiUsageException(
@@ -149,11 +158,6 @@ public void verify() {
"Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type; Offending method: %s",
method.toString()));
}
-
- if (hasParameterOfType(method, Sort.class)) {
- throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter;"
- + " Use sorting capabilities on Pageable instead; Offending method: %s", method));
- }
}
super.verify();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableFluentQuerySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableFluentQuerySupport.java
index 74abf97ab2..79d26a524f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableFluentQuerySupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableFluentQuerySupport.java
@@ -33,17 +33,21 @@ abstract class FetchableFluentQuerySupport implements FluentQuery.Fetchabl
private final P predicate;
private final Sort sort;
+
+ private final int limit;
+
private final Class resultType;
private final List fieldsToInclude;
- FetchableFluentQuerySupport(P predicate, Sort sort, Class resultType, List fieldsToInclude) {
+ FetchableFluentQuerySupport(P predicate, Sort sort, int limit, Class resultType, List fieldsToInclude) {
this.predicate = predicate;
this.sort = sort;
+ this.limit = limit;
this.resultType = resultType;
this.fieldsToInclude = fieldsToInclude;
}
- /*
+ /*
* (non-Javadoc)
* @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#sortBy(org.springframework.data.domain.Sort)
*/
@@ -52,10 +56,18 @@ public FluentQuery.FetchableFluentQuery sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
- return create(predicate, sort, resultType, fieldsToInclude);
+ return create(predicate, sort, limit, resultType, fieldsToInclude);
}
- /*
+ @Override
+ public FluentQuery.FetchableFluentQuery limit(int limit) {
+
+ Assert.isTrue(limit > 0, "Limit must be greater zero");
+
+ return create(predicate, sort, limit, resultType, fieldsToInclude);
+ }
+
+ /*
* (non-Javadoc)
* @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#as(java.lang.Class)
*/
@@ -64,10 +76,10 @@ public FluentQuery.FetchableFluentQuery as(Class projection) {
Assert.notNull(projection, "Projection target type must not be null");
- return create(predicate, sort, projection, fieldsToInclude);
+ return create(predicate, sort, limit, projection, fieldsToInclude);
}
- /*
+ /*
* (non-Javadoc)
* @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#project(java.util.Collection)
*/
@@ -76,11 +88,11 @@ public FluentQuery.FetchableFluentQuery project(Collection properties
Assert.notNull(properties, "Projection properties must not be null");
- return create(predicate, sort, resultType, new ArrayList<>(properties));
+ return create(predicate, sort, limit, resultType, new ArrayList<>(properties));
}
- protected abstract FetchableFluentQuerySupport create(P predicate, Sort sort, Class resultType,
- List fieldsToInclude);
+ protected abstract FetchableFluentQuerySupport create(P predicate, Sort sort, int limit,
+ Class resultType, List fieldsToInclude);
P getPredicate() {
return predicate;
@@ -90,6 +102,10 @@ Sort getSort() {
return sort;
}
+ int getLimit() {
+ return limit;
+ }
+
Class getResultType() {
return resultType;
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java
index 0399dfd5c8..f95092adb8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java
@@ -25,6 +25,8 @@
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Window;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
@@ -228,17 +230,17 @@ private SpringDataMongodbQuery applySorting(SpringDataMongodbQuery query,
class FluentQuerydsl extends FetchableFluentQuerySupport {
FluentQuerydsl(Predicate predicate, Class resultType) {
- this(predicate, Sort.unsorted(), resultType, Collections.emptyList());
+ this(predicate, Sort.unsorted(), 0, resultType, Collections.emptyList());
}
- FluentQuerydsl(Predicate predicate, Sort sort, Class resultType, List fieldsToInclude) {
- super(predicate, sort, resultType, fieldsToInclude);
+ FluentQuerydsl(Predicate predicate, Sort sort, int limit, Class resultType, List fieldsToInclude) {
+ super(predicate, sort, limit, resultType, fieldsToInclude);
}
@Override
- protected FluentQuerydsl create(Predicate predicate, Sort sort, Class resultType,
+ protected FluentQuerydsl create(Predicate predicate, Sort sort, int limit, Class resultType,
List fieldsToInclude) {
- return new FluentQuerydsl<>(predicate, sort, resultType, fieldsToInclude);
+ return new FluentQuerydsl<>(predicate, sort, limit, resultType, fieldsToInclude);
}
@Override
@@ -256,6 +258,11 @@ public List all() {
return createQuery().fetch();
}
+ @Override
+ public Window scroll(ScrollPosition scrollPosition) {
+ return createQuery().scroll(scrollPosition);
+ }
+
@Override
public Page page(Pageable pageable) {
@@ -296,6 +303,8 @@ private void customize(BasicQuery query) {
if (getSort().isSorted()) {
query.with(getSort());
}
+
+ query.limit(getLimit());
}
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveFluentQuerySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveFluentQuerySupport.java
index 505a7c0c44..9147243648 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveFluentQuerySupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveFluentQuerySupport.java
@@ -33,12 +33,14 @@ abstract class ReactiveFluentQuerySupport implements FluentQuery.ReactiveF
private final P predicate;
private final Sort sort;
+ private final int limit;
private final Class resultType;
private final List fieldsToInclude;
- ReactiveFluentQuerySupport(P predicate, Sort sort, Class resultType, List fieldsToInclude) {
+ ReactiveFluentQuerySupport(P predicate, Sort sort, int limit, Class resultType, List fieldsToInclude) {
this.predicate = predicate;
this.sort = sort;
+ this.limit = limit;
this.resultType = resultType;
this.fieldsToInclude = fieldsToInclude;
}
@@ -52,7 +54,15 @@ public ReactiveFluentQuery sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
- return create(predicate, sort, resultType, fieldsToInclude);
+ return create(predicate, sort, limit, resultType, fieldsToInclude);
+ }
+
+ @Override
+ public ReactiveFluentQuery limit(int limit) {
+
+ Assert.isTrue(limit > 0, "Limit must be greater zero");
+
+ return create(predicate, sort, limit, resultType, fieldsToInclude);
}
/*
@@ -64,7 +74,7 @@ public ReactiveFluentQuery as(Class projection) {
Assert.notNull(projection, "Projection target type must not be null");
- return create(predicate, sort, projection, fieldsToInclude);
+ return create(predicate, sort, limit, projection, fieldsToInclude);
}
/*
@@ -76,10 +86,10 @@ public ReactiveFluentQuery project(Collection properties) {
Assert.notNull(properties, "Projection properties must not be null");
- return create(predicate, sort, resultType, new ArrayList<>(properties));
+ return create(predicate, sort, limit, resultType, new ArrayList<>(properties));
}
- protected abstract ReactiveFluentQuerySupport create(P predicate, Sort sort, Class resultType,
+ protected abstract ReactiveFluentQuerySupport create(P predicate, Sort sort, int limit, Class resultType,
List fieldsToInclude);
P getPredicate() {
@@ -90,6 +100,10 @@ Sort getSort() {
return sort;
}
+ int getLimit() {
+ return limit;
+ }
+
Class getResultType() {
return resultType;
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java
index a21bbb6c17..d2269fe69a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java
@@ -26,6 +26,8 @@
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Window;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
@@ -195,17 +197,18 @@ private ReactiveSpringDataMongodbQuery applySorting(ReactiveSpringDataMongodb
class ReactiveFluentQuerydsl extends ReactiveFluentQuerySupport {
ReactiveFluentQuerydsl(Predicate predicate, Class resultType) {
- this(predicate, Sort.unsorted(), resultType, Collections.emptyList());
+ this(predicate, Sort.unsorted(), 0, resultType, Collections.emptyList());
}
- ReactiveFluentQuerydsl(Predicate predicate, Sort sort, Class resultType, List fieldsToInclude) {
- super(predicate, sort, resultType, fieldsToInclude);
+ ReactiveFluentQuerydsl(Predicate predicate, Sort sort, int limit, Class resultType,
+ List fieldsToInclude) {
+ super(predicate, sort, limit, resultType, fieldsToInclude);
}
@Override
- protected ReactiveFluentQuerydsl create(Predicate predicate, Sort sort, Class resultType,
+ protected ReactiveFluentQuerydsl create(Predicate predicate, Sort sort, int limit, Class resultType,
List fieldsToInclude) {
- return new ReactiveFluentQuerydsl<>(predicate, sort, resultType, fieldsToInclude);
+ return new ReactiveFluentQuerydsl<>(predicate, sort, limit, resultType, fieldsToInclude);
}
@Override
@@ -223,6 +226,11 @@ public Flux all() {
return createQuery().fetch();
}
+ @Override
+ public Mono> scroll(ScrollPosition scrollPosition) {
+ return createQuery().scroll(scrollPosition);
+ }
+
@Override
public Mono