Skip to content

Commit c71cc05

Browse files
committed
Add support for reverse scrolling.
Closes #4325
1 parent 1b85cfe commit c71cc05

File tree

2 files changed

+54
-20
lines changed

2 files changed

+54
-20
lines changed

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

+21-12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.bson.BsonNull;
2424
import org.bson.Document;
2525
import org.springframework.data.domain.KeysetScrollPosition;
26+
import org.springframework.data.domain.KeysetScrollPosition.Direction;
2627
import org.springframework.data.domain.ScrollPosition;
2728
import org.springframework.data.domain.Window;
2829
import org.springframework.data.mongodb.core.EntityOperations.Entity;
@@ -46,6 +47,10 @@ class ScrollUtils {
4647
*/
4748
static KeySetScrollQuery createKeysetPaginationQuery(Query query, String idPropertyName) {
4849

50+
KeysetScrollPosition keyset = query.getKeyset();
51+
Map<String, Object> keysetValues = keyset.getKeys();
52+
Document queryObject = query.getQueryObject();
53+
4954
Document sortObject = query.isSorted() ? query.getSortObject() : new Document();
5055
sortObject.put(idPropertyName, 1);
5156

@@ -57,13 +62,7 @@ static KeySetScrollQuery createKeysetPaginationQuery(Query query, String idPrope
5762
}
5863
}
5964

60-
Document queryObject = query.getQueryObject();
61-
6265
List<Document> or = (List<Document>) queryObject.getOrDefault("$or", new ArrayList<>());
63-
64-
// TODO: reverse scrolling
65-
Map<String, Object> keysetValues = query.getKeyset().getKeys();
66-
Document keysetSort = new Document();
6766
List<String> sortKeys = new ArrayList<>(sortObject.keySet());
6867

6968
if (!keysetValues.isEmpty() && !keysetValues.keySet().containsAll(sortKeys)) {
@@ -86,10 +85,11 @@ static KeySetScrollQuery createKeysetPaginationQuery(Query query, String idPrope
8685
Object o = keysetValues.get(sortSegment);
8786

8887
if (j >= i) { // tail segment
89-
if(o instanceof BsonNull) {
90-
throw new IllegalStateException("Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(sortSegment));
88+
if (o instanceof BsonNull) {
89+
throw new IllegalStateException(
90+
"Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'".formatted(sortSegment));
9191
}
92-
sortConstraint.put(sortSegment, new Document(sortOrder == 1 ? "$gt" : "$lt", o));
92+
sortConstraint.put(sortSegment, new Document(getComparator(sortOrder, keyset.getDirection()), o));
9393
break;
9494
}
9595

@@ -102,16 +102,25 @@ static KeySetScrollQuery createKeysetPaginationQuery(Query query, String idPrope
102102
}
103103
}
104104

105-
if (!keysetSort.isEmpty()) {
106-
or.add(keysetSort);
107-
}
108105
if (!or.isEmpty()) {
109106
queryObject.put("$or", or);
110107
}
111108

112109
return new KeySetScrollQuery(queryObject, fieldsObject, sortObject);
113110
}
114111

112+
private static String getComparator(int sortOrder, Direction direction) {
113+
114+
// use gte/lte to include the object at the cursor/keyset so that
115+
// we can include it in the result to check whether there is a next object.
116+
// It needs to be filtered out later on.
117+
if (direction == Direction.Backward) {
118+
return sortOrder == 0 ? "$gte" : "$lte";
119+
}
120+
121+
return sortOrder == 1 ? "$gt" : "$lt";
122+
}
123+
115124
static <T> Window<T> createWindow(Document sortObject, int limit, List<T> result, EntityOperations operations) {
116125

117126
IntFunction<KeysetScrollPosition> positionFunction = value -> {

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java

+33-8
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb.core;
1717

18+
import static org.assertj.core.api.Assertions.*;
1819
import static org.springframework.data.mongodb.core.query.Criteria.*;
19-
import static org.springframework.data.mongodb.test.util.Assertions.*;
2020

2121
import lombok.Data;
2222
import lombok.NoArgsConstructor;
@@ -37,6 +37,7 @@
3737
import org.springframework.data.annotation.PersistenceCreator;
3838
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
3939
import org.springframework.data.domain.KeysetScrollPosition;
40+
import org.springframework.data.domain.KeysetScrollPosition.Direction;
4041
import org.springframework.data.domain.OffsetScrollPosition;
4142
import org.springframework.data.domain.ScrollPosition;
4243
import org.springframework.data.domain.Sort;
@@ -118,7 +119,7 @@ void shouldUseKeysetScrollingWithNestedSort() {
118119
assertThat(scroll).hasSize(2);
119120
assertThat(scroll).containsOnly(john20, john40);
120121

121-
scroll = template.scroll(q.with(scroll.positionAt(scroll.size()-1)), WithNestedDocument.class);
122+
scroll = template.scroll(q.with(scroll.positionAt(scroll.size() - 1)), WithNestedDocument.class);
122123

123124
assertThat(scroll.hasNext()).isFalse();
124125
assertThat(scroll.isLast()).isTrue();
@@ -143,6 +144,19 @@ void shouldErrorOnNullValueForQuery() {
143144
new Document("name", "foo"));
144145

145146
template.insertAll(Arrays.asList(john20, john40, john41, john42, john43, john44));
147+
}
148+
149+
@Test // GH-4308
150+
void shouldAllowReverseSort() {
151+
152+
WithNestedDocument john20 = new WithNestedDocument(null, "John", 120, new WithNestedDocument("John", 20),
153+
new Document("name", "bar"));
154+
WithNestedDocument john40 = new WithNestedDocument(null, "John", 140, new WithNestedDocument("John", 40),
155+
new Document("name", "baz"));
156+
WithNestedDocument john41 = new WithNestedDocument(null, "John", 141, new WithNestedDocument("John", 41),
157+
new Document("name", "foo"));
158+
159+
template.insertAll(Arrays.asList(john20, john40, john41));
146160

147161
Query q = new Query(where("name").regex("J.*")).with(Sort.by("nested.name", "nested.age", "document.name"))
148162
.limit(2);
@@ -155,11 +169,22 @@ void shouldErrorOnNullValueForQuery() {
155169
assertThat(scroll).hasSize(2);
156170
assertThat(scroll).containsOnly(john20, john40);
157171

158-
ScrollPosition startAfter = scroll.positionAt(scroll.size()-1);
172+
scroll = template.scroll(q.with(scroll.positionAt(scroll.size() - 1)), WithNestedDocument.class);
159173

160-
assertThatExceptionOfType(IllegalStateException.class)
161-
.isThrownBy(() -> template.scroll(q.with(startAfter), WithNestedDocument.class))
162-
.withMessageContaining("document.name");
174+
assertThat(scroll.hasNext()).isFalse();
175+
assertThat(scroll.isLast()).isTrue();
176+
assertThat(scroll).hasSize(1);
177+
assertThat(scroll).containsOnly(john41);
178+
179+
KeysetScrollPosition scrollPosition = (KeysetScrollPosition) scroll.positionAt(0);
180+
KeysetScrollPosition reversePosition = KeysetScrollPosition.of(scrollPosition.getKeys(), Direction.Backward);
181+
182+
scroll = template.scroll(q.with(reversePosition), WithNestedDocument.class);
183+
184+
assertThat(scroll.hasNext()).isTrue();
185+
assertThat(scroll.isLast()).isFalse();
186+
assertThat(scroll).hasSize(2);
187+
assertThat(scroll).containsOnly(john20, john40);
163188
}
164189

165190
@ParameterizedTest // GH-4308
@@ -185,15 +210,15 @@ public <T> void shouldApplyCursoringCorrectly(ScrollPosition scrollPosition, Cla
185210
assertThat(scroll).hasSize(2);
186211
assertThat(scroll).containsOnly(assertionConverter.apply(jane_20), assertionConverter.apply(jane_40));
187212

188-
scroll = template.scroll(q.with(scroll.positionAt(scroll.size()-1)).limit(3), resultType, "person");
213+
scroll = template.scroll(q.with(scroll.positionAt(scroll.size() - 1)).limit(3), resultType, "person");
189214

190215
assertThat(scroll.hasNext()).isTrue();
191216
assertThat(scroll.isLast()).isFalse();
192217
assertThat(scroll).hasSize(3);
193218
assertThat(scroll).contains(assertionConverter.apply(jane_42), assertionConverter.apply(john20));
194219
assertThat(scroll).containsAnyOf(assertionConverter.apply(john40_1), assertionConverter.apply(john40_2));
195220

196-
scroll = template.scroll(q.with(scroll.positionAt(scroll.size()-1)).limit(1), resultType, "person");
221+
scroll = template.scroll(q.with(scroll.positionAt(scroll.size() - 1)).limit(1), resultType, "person");
197222

198223
assertThat(scroll.hasNext()).isFalse();
199224
assertThat(scroll.isLast()).isTrue();

0 commit comments

Comments
 (0)