Skip to content

Commit c9831f7

Browse files
committed
Add support for imperative CursorWindow query methods.
1 parent 699b417 commit c9831f7

File tree

9 files changed

+106
-18
lines changed

9 files changed

+106
-18
lines changed

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.util.stream.Stream;
2121

2222
import org.springframework.dao.DataAccessException;
23+
import org.springframework.data.domain.CursorRequest;
24+
import org.springframework.data.domain.CursorWindow;
2325
import org.springframework.data.geo.GeoResults;
2426
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
2527
import org.springframework.data.mongodb.core.query.NearQuery;
@@ -124,12 +126,19 @@ default Optional<T> first() {
124126
Stream<T> stream();
125127

126128
/**
127-
* Get the number of matching elements.
128-
* <br />
129-
* This method uses an {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions) aggregation
130-
* execution} even for empty {@link Query queries} which may have an impact on performance, but guarantees shard,
131-
* session and transaction compliance. In case an inaccurate count satisfies the applications needs use
132-
* {@link MongoOperations#estimatedCount(String)} for empty queries instead.
129+
* Return a window from a {@link CursorRequest}.
130+
*
131+
* @return a window from the resulting elements.
132+
*/
133+
CursorWindow<T> window(CursorRequest cursorRequest);
134+
135+
/**
136+
* Get the number of matching elements. <br />
137+
* This method uses an
138+
* {@link com.mongodb.client.MongoCollection#countDocuments(org.bson.conversions.Bson, com.mongodb.client.model.CountOptions)
139+
* aggregation execution} even for empty {@link Query queries} which may have an impact on performance, but
140+
* guarantees shard, session and transaction compliance. In case an inaccurate count satisfies the applications
141+
* needs use {@link MongoOperations#estimatedCount(String)} for empty queries instead.
133142
*
134143
* @return total number of matching elements.
135144
*/

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import org.bson.Document;
2323
import org.springframework.dao.IncorrectResultSizeDataAccessException;
24+
import org.springframework.data.domain.CursorRequest;
25+
import org.springframework.data.domain.CursorWindow;
2426
import org.springframework.data.mongodb.core.query.NearQuery;
2527
import org.springframework.data.mongodb.core.query.Query;
2628
import org.springframework.data.mongodb.core.query.SerializationUtils;
@@ -138,6 +140,11 @@ public Stream<T> stream() {
138140
return doStream();
139141
}
140142

143+
@Override
144+
public CursorWindow<T> window(CursorRequest cursorRequest) {
145+
return template.doFindWindow(cursorRequest, query, domainType, returnType, getCollectionName());
146+
}
147+
141148
@Override
142149
public TerminatingFindNear<T> near(NearQuery nearQuery) {
143150
return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType);

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -868,18 +868,27 @@ public <T> CursorWindow<T> findWindow(CursorRequest cursorRequest, Query query,
868868
public <T> CursorWindow<T> findWindow(CursorRequest cursorRequest, Query query, Class<T> entityType,
869869
String collectionName) {
870870

871+
return doFindWindow(cursorRequest, query, entityType, entityType, collectionName);
872+
}
873+
874+
<T> CursorWindow<T> doFindWindow(CursorRequest cursorRequest, Query query, Class<?> sourceClass, Class<T> targetClass,
875+
String collectionName) {
876+
871877
Assert.notNull(cursorRequest, "CursorRequest must not be null");
872878
Assert.notNull(query, "Query must not be null");
873879
Assert.notNull(collectionName, "CollectionName must not be null");
874-
Assert.notNull(entityType, "Entity type must not be null");
880+
Assert.notNull(sourceClass, "Entity type must not be null");
881+
Assert.notNull(targetClass, "Target type must not be null");
875882

883+
ReadDocumentCallback<T> callback = new ReadDocumentCallback<>(mongoConverter, targetClass, collectionName);
876884
if (cursorRequest instanceof OffsetCursorRequest offset) {
877885

878886
int limit = offset.getSize() + 1;
879887

880888
List<T> result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(),
881-
entityType, new QueryCursorPreparer(query, new Query().with(cursorRequest.getSort()).getSortObject(), limit,
882-
offset.getOffset(), entityType));
889+
sourceClass, new QueryCursorPreparer(query, new Query().with(cursorRequest.getSort()).getSortObject(), limit,
890+
offset.getOffset(), sourceClass),
891+
callback);
883892

884893
return CursorUtils.createWindow(cursorRequest, result);
885894
}
@@ -889,11 +898,11 @@ entityType, new QueryCursorPreparer(query, new Query().with(cursorRequest.getSor
889898
int limit = keyset.getSize() + 1;
890899

891900
KeySetCursorQuery keysetPaginationQuery = CursorUtils.createKeysetPaginationQuery(query, keyset,
892-
operations.getIdPropertyName(entityType));
901+
operations.getIdPropertyName(sourceClass));
893902

894903
List<T> result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(),
895-
keysetPaginationQuery.fields(), entityType,
896-
new QueryCursorPreparer(query, keysetPaginationQuery.sort(), limit, 0, entityType));
904+
keysetPaginationQuery.fields(), sourceClass,
905+
new QueryCursorPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback);
897906

898907
return CursorUtils.createWindow(keyset, result, operations);
899908
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, F
167167
return q -> operation.matching(q).stream();
168168
} else if (method.isCollectionQuery()) {
169169
return q -> operation.matching(q.with(accessor.getPageable()).with(accessor.getSort())).all();
170+
} else if (method.isCursorWindowQuery()) {
171+
return q -> operation.matching(q).window(accessor.getCursorRequest());
170172
} else if (method.isPageQuery()) {
171173
return new PagedExecution(operation, accessor.getPageable());
172174
} else if (isCountQuery()) {

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Iterator;
2222
import java.util.List;
2323

24+
import org.springframework.data.domain.CursorRequest;
2425
import org.springframework.data.domain.Pageable;
2526
import org.springframework.data.domain.Range;
2627
import org.springframework.data.domain.Sort;
@@ -71,6 +72,11 @@ public PotentiallyConvertingIterator iterator() {
7172
return new ConvertingIterator(delegate.iterator());
7273
}
7374

75+
@Override
76+
public CursorRequest getCursorRequest() {
77+
return delegate.getCursorRequest();
78+
}
79+
7480
public Pageable getPageable() {
7581
return delegate.getPageable();
7682
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,8 @@ public boolean hasAnnotatedCollation() {
335335
public String getAnnotatedCollation() {
336336

337337
return doFindAnnotation(Collation.class).map(Collation::value) //
338-
.orElseThrow(() -> new IllegalStateException(
339-
"Expected to find @Collation annotation but did not; Make sure to check hasAnnotatedCollation() before."));
338+
.orElseThrow(() -> new IllegalStateException(
339+
"Expected to find @Collation annotation but did not; Make sure to check hasAnnotatedCollation() before."));
340340
}
341341

342342
/**
@@ -420,7 +420,8 @@ public void verify() {
420420

421421
if (isModifyingQuery()) {
422422

423-
if (isCollectionQuery() || isSliceQuery() || isPageQuery() || isGeoNearQuery() || !isNumericOrVoidReturnValue()) { //
423+
if (isCollectionQuery() || isCursorWindowQuery() || isSliceQuery() || isPageQuery() || isGeoNearQuery()
424+
|| !isNumericOrVoidReturnValue()) { //
424425
throw new IllegalStateException(
425426
String.format("Update method may be void or return a numeric value (the number of updated documents)."
426427
+ "Offending method: %s", method));

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@
3838
import org.junit.jupiter.api.Test;
3939
import org.junit.jupiter.api.TestInstance;
4040
import org.junit.jupiter.api.extension.ExtendWith;
41-
4241
import org.springframework.beans.factory.annotation.Autowired;
4342
import org.springframework.dao.DuplicateKeyException;
4443
import org.springframework.dao.IncorrectResultSizeDataAccessException;
44+
import org.springframework.data.domain.CursorWindow;
4545
import org.springframework.data.domain.Example;
46+
import org.springframework.data.domain.OffsetCursorRequest;
4647
import org.springframework.data.domain.Page;
4748
import org.springframework.data.domain.PageRequest;
4849
import org.springframework.data.domain.Pageable;
@@ -208,6 +209,29 @@ void findsPagedPersons() {
208209
assertThat(result).contains(dave, stefan);
209210
}
210211

212+
@Test // GH-4308
213+
void appliesCursorWindowingCorrectly() {
214+
215+
CursorWindow<Person> page = repository.findByLastnameLike("*a*",
216+
OffsetCursorRequest.ofSize(1, Sort.by(Direction.ASC, "lastname", "firstname")));
217+
assertThat(page.isFirst()).isTrue();
218+
assertThat(page.isLast()).isFalse();
219+
assertThat(page.size()).isEqualTo(1);
220+
assertThat(page).contains(carter);
221+
}
222+
223+
@Test // GH-4308
224+
void appliesCursorWindowingWithProjectionCorrectly() {
225+
226+
CursorWindow<PersonSummaryDto> page = repository.findCursorProjectionByLastnameLike("*a*",
227+
OffsetCursorRequest.ofSize(2, Sort.by(Direction.ASC, "lastname", "firstname")));
228+
assertThat(page.isFirst()).isTrue();
229+
assertThat(page.isLast()).isFalse();
230+
assertThat(page.size()).isEqualTo(2);
231+
232+
assertThat(page).element(0).isEqualTo(new PersonSummaryDto(carter.getFirstname(), carter.getLastname()));
233+
}
234+
211235
@Test
212236
void executesPagedFinderCorrectly() {
213237

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.regex.Pattern;
2424
import java.util.stream.Stream;
2525

26+
import org.springframework.data.domain.CursorRequest;
27+
import org.springframework.data.domain.CursorWindow;
2628
import org.springframework.data.domain.Page;
2729
import org.springframework.data.domain.Pageable;
2830
import org.springframework.data.domain.Range;
@@ -115,7 +117,26 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
115117
List<Person> findByAgeLessThan(int age, Sort sort);
116118

117119
/**
118-
* Returns a page of {@link Person}s with a lastname mathing the given one (*-wildcards supported).
120+
* Returns a cursor window of {@link Person}s with a lastname matching the given one (*-wildcards supported).
121+
*
122+
* @param lastname
123+
* @param cursorRequest
124+
* @return
125+
*/
126+
CursorWindow<Person> findByLastnameLike(String lastname, CursorRequest cursorRequest);
127+
128+
/**
129+
* Returns a cursor window of {@link Person}s applying projections with a lastname matching the given one (*-wildcards
130+
* supported).
131+
*
132+
* @param lastname
133+
* @param cursorRequest
134+
* @return
135+
*/
136+
CursorWindow<PersonSummaryDto> findCursorProjectionByLastnameLike(String lastname, CursorRequest cursorRequest);
137+
138+
/**
139+
* Returns a page of {@link Person}s with a lastname matching the given one (*-wildcards supported).
119140
*
120141
* @param lastname
121142
* @param pageable
@@ -429,7 +450,7 @@ Person findPersonByManyArguments(String firstname, String lastname, String email
429450
@Update("{ '$inc' : { 'visits' : ?1 } }")
430451
int updateAllByLastname(String lastname, int increment);
431452

432-
@Update( pipeline = {"{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }"})
453+
@Update(pipeline = { "{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }" })
433454
void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment);
434455

435456
@Update("{ '$inc' : { 'visits' : ?#{[1]} } }")

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonSummaryDto.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,18 @@
1515
*/
1616
package org.springframework.data.mongodb.repository;
1717

18+
import lombok.AllArgsConstructor;
19+
import lombok.EqualsAndHashCode;
20+
import lombok.NoArgsConstructor;
21+
import lombok.ToString;
22+
1823
/**
1924
* @author Oliver Gierke
2025
*/
26+
@EqualsAndHashCode
27+
@AllArgsConstructor
28+
@NoArgsConstructor
29+
@ToString
2130
public class PersonSummaryDto {
2231

2332
String firstname;

0 commit comments

Comments
 (0)