Skip to content

Commit 699b417

Browse files
committed
Add support for reactive cursor windowing.
1 parent 446ebe7 commit 699b417

File tree

7 files changed

+495
-192
lines changed

7 files changed

+495
-192
lines changed

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

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,24 @@
1515
*/
1616
package org.springframework.data.mongodb.core;
1717

18+
import java.util.ArrayList;
1819
import java.util.List;
20+
import java.util.Map;
1921

22+
import org.bson.Document;
2023
import org.springframework.data.domain.CursorRequest;
2124
import org.springframework.data.domain.CursorWindow;
25+
import org.springframework.data.domain.KeysetCursorRequest;
26+
import org.springframework.data.domain.Sort;
27+
import org.springframework.data.domain.Sort.Order;
28+
import org.springframework.data.mongodb.core.EntityOperations.Entity;
29+
import org.springframework.data.mongodb.core.query.Query;
2230

2331
/**
32+
* Utilities to run cursor window queries and create cursor window results.
33+
*
2434
* @author Mark Paluch
35+
* @since 4.1
2536
*/
2637
class CursorUtils {
2738

@@ -42,7 +53,104 @@ static <T> List<T> getSubList(CursorRequest request, List<T> result) {
4253
return result;
4354
}
4455

45-
public static <T> CursorWindow<T> createWindow(CursorRequest cursorRequest, List<T> result) {
56+
/**
57+
* Create the actual query to run keyset-based pagination. Affects projection, sorting, and the criteria.
58+
*
59+
* @param query
60+
* @param cursorRequest
61+
* @param idPropertyName
62+
* @return
63+
*/
64+
static KeySetCursorQuery createKeysetPaginationQuery(Query query, KeysetCursorRequest cursorRequest,
65+
String idPropertyName) {
66+
67+
Sort targetSort = cursorRequest.getSort().and(Sort.by(Order.asc(idPropertyName)));
68+
69+
// make sure we can extract the keyset
70+
Document fieldsObject = query.getFieldsObject();
71+
if (!fieldsObject.isEmpty()) {
72+
fieldsObject.put(idPropertyName, 1);
73+
for (Order order : cursorRequest.getSort()) {
74+
fieldsObject.put(order.getProperty(), 1);
75+
}
76+
}
77+
78+
Document sortObject = query.isSorted() ? query.getSortObject() : new Document();
79+
targetSort.forEach(order -> sortObject.put(order.getProperty(), order.isAscending() ? 1 : -1));
80+
sortObject.put(idPropertyName, 1);
81+
82+
Document queryObject = query.getQueryObject();
83+
84+
List<Document> or = (List<Document>) queryObject.getOrDefault("$or", new ArrayList<>());
85+
86+
Map<String, Object> keysetValues = cursorRequest.getKeys();
87+
Document keysetSort = new Document();
88+
List<String> sortKeys = new ArrayList<>(sortObject.keySet());
89+
90+
if (!keysetValues.isEmpty() && !keysetValues.keySet().containsAll(sortKeys)) {
91+
throw new IllegalStateException("KeysetCursorRequest does not contain all keyset values");
92+
}
93+
94+
// first query doesn't come with a keyset
95+
if (!keysetValues.isEmpty()) {
96+
97+
// build matrix query for keyset paging that contains sort^2 queries
98+
// reflecting a query that follows sort order semantics starting from the last returned keyset
99+
for (int i = 0; i < sortKeys.size(); i++) {
100+
101+
Document sortConstraint = new Document();
102+
103+
for (int j = 0; j < sortKeys.size(); j++) {
104+
105+
String sortSegment = sortKeys.get(j);
106+
int sortOrder = sortObject.getInteger(sortSegment);
107+
Object o = keysetValues.get(sortSegment);
108+
109+
if (j >= i) { // tail segment
110+
sortConstraint.put(sortSegment, new Document(sortOrder == 1 ? "$gt" : "$lt", o));
111+
break;
112+
}
113+
114+
sortConstraint.put(sortSegment, o);
115+
}
116+
117+
if (!sortConstraint.isEmpty()) {
118+
or.add(sortConstraint);
119+
}
120+
}
121+
}
122+
123+
if (!keysetSort.isEmpty()) {
124+
or.add(keysetSort);
125+
}
126+
if (!or.isEmpty()) {
127+
queryObject.put("$or", or);
128+
}
129+
130+
return new KeySetCursorQuery(queryObject, fieldsObject, sortObject);
131+
}
132+
133+
static <T> CursorWindow<T> createWindow(KeysetCursorRequest cursorRequest, List<T> result,
134+
EntityOperations operations) {
135+
136+
if (CursorUtils.hasMoreElements(cursorRequest, result)) {
137+
138+
T last = result.get(cursorRequest.getSize() - 1);
139+
Entity<T> entity = operations.forEntity(last);
140+
141+
Map<String, Object> keys = entity.extractKeys(cursorRequest.getSort());
142+
cursorRequest = cursorRequest.withNext(keys);
143+
}
144+
145+
return createWindow(cursorRequest, result);
146+
}
147+
148+
static <T> CursorWindow<T> createWindow(CursorRequest cursorRequest, List<T> result) {
46149
return CursorWindow.from(cursorRequest.withLast(isLast(cursorRequest, result)), getSubList(cursorRequest, result));
47150
}
151+
152+
record KeySetCursorQuery(Document query, Document fields, Document sort) {
153+
154+
}
155+
48156
}

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

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -470,38 +470,6 @@ MongoCollection<Document> createView(String name, String source, AggregationPipe
470470
*/
471471
<T> List<T> findAll(Class<T> entityClass, String collectionName);
472472

473-
/**
474-
* Query for a cursor window of objects of type T from the specified collection. <br />
475-
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
476-
* configured otherwise, an instance of {@link MappingMongoConverter} will be used. <br />
477-
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
478-
* to map objects since the test for class type is done in the client and not on the server.
479-
*
480-
* @param cursorRequest the cursor request.
481-
* @param entityType the parametrized type of the returned list.
482-
* @param collectionName name of the collection to retrieve the objects from.
483-
* @return the converted cursor window.
484-
* @see org.springframework.data.domain.OffsetCursorRequest
485-
* @see org.springframework.data.domain.KeysetCursorRequest
486-
*/
487-
<T> CursorWindow<T> findWindow(CursorRequest cursorRequest, Query query, Class<T> entityTypee);
488-
489-
/**
490-
* Query for a cursor window of objects of type T from the specified collection. <br />
491-
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
492-
* configured otherwise, an instance of {@link MappingMongoConverter} will be used. <br />
493-
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
494-
* to map objects since the test for class type is done in the client and not on the server.
495-
*
496-
* @param cursorRequest the cursor request.
497-
* @param entityType the parametrized type of the returned list.
498-
* @param collectionName name of the collection to retrieve the objects from.
499-
* @return the converted cursor window.
500-
* @see org.springframework.data.domain.OffsetCursorRequest
501-
* @see org.springframework.data.domain.KeysetCursorRequest
502-
*/
503-
<T> CursorWindow<T> findWindow(CursorRequest cursorRequest, Query query, Class<T> entityType, String collectionName);
504-
505473
/**
506474
* Execute an aggregation operation. The raw results will be mapped to the given entity class. The name of the
507475
* inputCollection is derived from the inputType of the aggregation.
@@ -838,6 +806,43 @@ <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, Strin
838806
*/
839807
<T> List<T> find(Query query, Class<T> entityClass, String collectionName);
840808

809+
/**
810+
* Query for a cursor window of objects of type T from the specified collection. <br />
811+
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
812+
* configured otherwise, an instance of {@link MappingMongoConverter} will be used. <br />
813+
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
814+
* to map objects since the test for class type is done in the client and not on the server.
815+
*
816+
* @param cursorRequest the cursor request.
817+
* @param query the query class that specifies the criteria used to find a record and also an optional fields
818+
* specification. Must not be {@literal null}.
819+
* @param entityType the parametrized type of the returned list.
820+
* @return the converted cursor window.
821+
* @since 4.1
822+
* @see org.springframework.data.domain.OffsetCursorRequest
823+
* @see org.springframework.data.domain.KeysetCursorRequest
824+
*/
825+
<T> CursorWindow<T> findWindow(CursorRequest cursorRequest, Query query, Class<T> entityType);
826+
827+
/**
828+
* Query for a cursor window of objects of type T from the specified collection. <br />
829+
* The object is converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless
830+
* configured otherwise, an instance of {@link MappingMongoConverter} will be used. <br />
831+
* If your collection does not contain a homogeneous collection of types, this operation will not be an efficient way
832+
* to map objects since the test for class type is done in the client and not on the server.
833+
*
834+
* @param cursorRequest the cursor request.
835+
* @param query the query class that specifies the criteria used to find a record and also an optional fields
836+
* specification. Must not be {@literal null}.
837+
* @param entityType the parametrized type of the returned list.
838+
* @param collectionName name of the collection to retrieve the objects from.
839+
* @return the converted cursor window.
840+
* @since 4.1
841+
* @see org.springframework.data.domain.OffsetCursorRequest
842+
* @see org.springframework.data.domain.KeysetCursorRequest
843+
*/
844+
<T> CursorWindow<T> findWindow(CursorRequest cursorRequest, Query query, Class<T> entityType, String collectionName);
845+
841846
/**
842847
* Returns a document with the given id mapped onto the given class. The collection the query is ran against will be
843848
* derived from the given target class as well.

0 commit comments

Comments
 (0)