diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index cebdf4e40..dd2b78de5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -36,6 +36,7 @@ import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.util.SliceUtils; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -87,7 +88,7 @@ interface MongoQueryExecution { int pageSize = pageable.getPageSize(); // Apply Pageable but tweak limit to peek into next page - Query modifiedQuery = query.with(pageable).limit(pageSize + 1); + Query modifiedQuery = SliceUtils.limitResult(query, pageable).with(pageable.getSort()); List result = find.matching(modifiedQuery).all(); boolean hasNext = result.size() > pageSize; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java index 042751d30..cf5191fd4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.support; +import org.springframework.data.mongodb.repository.util.SliceUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -113,14 +114,16 @@ class ReactiveSpringDataMongodbQuery extends SpringDataMongodbQuerySupport> fetchSlice(Pageable pageable) { - Mono> content = createQuery().map(it -> SliceUtils.getQuery(it, pageable)) + Mono> content = createQuery().map(it -> SliceUtils.limitResult(it, pageable).with(pageable.getSort())) .flatMapMany(it -> find.matching(it).all()).collectList(); - return content.map(it -> SliceUtils.getSlice(it, pageable)); + return content.map(it -> SliceUtils.sliceResult(it, pageable)); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index b62bc73f1..2f4c30ee7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -43,6 +43,7 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.mongodb.repository.util.SliceUtils; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; @@ -460,9 +461,9 @@ public class SimpleMongoRepository implements MongoRepository { Assert.notNull(pageable, "Pageable must not be null"); - List resultList = createQuery(q -> SliceUtils.getQuery(q, pageable)).all(); + List resultList = createQuery(q -> SliceUtils.limitResult(q, pageable).with(pageable.getSort())).all(); - return SliceUtils.getSlice(resultList, pageable); + return SliceUtils.sliceResult(resultList, pageable); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java index af7a5f57c..1c1df2c9a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.repository.support; import static org.springframework.data.mongodb.core.query.Criteria.*; +import org.springframework.data.mongodb.repository.util.SliceUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -596,8 +597,8 @@ public class SimpleReactiveMongoRepository implement @Override public Mono> slice(Pageable pageable) { - return createQuery(q -> SliceUtils.getQuery(q, pageable)).all().collectList() - .map(it -> SliceUtils.getSlice(it, pageable)); + return createQuery(q -> SliceUtils.limitResult(q, pageable).with(pageable.getSort())).all().collectList() + .map(it -> SliceUtils.sliceResult(it, pageable)); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index cc5f44de2..0ef6c3874 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -34,6 +34,7 @@ import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.util.SliceUtils; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.lang.Nullable; @@ -187,14 +188,15 @@ public class SpringDataMongodbQuery extends SpringDataMongodbQuerySupport fetchSlice(Pageable pageable) { - List content = find.matching(SliceUtils.getQuery(createQuery(), pageable)).all(); + List content = find.matching(SliceUtils.limitResult(createQuery(), pageable).with(pageable.getSort())).all(); - return SliceUtils.getSlice(content, pageable); + return SliceUtils.sliceResult(content, pageable); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SliceUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/util/SliceUtils.java similarity index 51% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SliceUtils.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/util/SliceUtils.java index 167875481..b570687cb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SliceUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/util/SliceUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.repository.support; +package org.springframework.data.mongodb.repository.util; import java.util.List; @@ -26,19 +26,21 @@ import org.springframework.data.mongodb.core.query.Query; * Utility methods for {@link Slice} handling. * * @author Mark Paluch + * @author Christoph Strobl * @since 4.5 */ -class SliceUtils { +public class SliceUtils { /** * Creates a {@link Slice} given {@link Pageable} and {@link List} of results. * - * @param - * @param resultList - * @param pageable - * @return + * @param the element type. + * @param resultList the source list holding the result of the request. If the result list contains more elements + * (indicating a next slice is available) it is trimmed to the {@link Pageable#getPageSize() page size}. + * @param pageable the source pageable. + * @return new instance of {@link Slice}. */ - public static Slice getSlice(List resultList, Pageable pageable) { + public static Slice sliceResult(List resultList, Pageable pageable) { boolean hasNext = resultList.size() > pageable.getPageSize(); @@ -50,16 +52,23 @@ class SliceUtils { } /** - * Customize query for slice retrieval. + * Customize query for {@link #sliceResult sliced result} retrieval. If {@link Pageable#isPaged() paged} the + * {@link Query#limit(int) limit} is set to {@code pagesize + 1} in order to determine if more data is available. * - * @param query - * @param pageable - * @return + * @param query the source query + * @param pageable paging to apply. + * @return new instance of {@link Query} if either {@link Pageable#isPaged() paged}, the source query otherwise. */ - public static Query getQuery(Query query, Pageable pageable) { + public static Query limitResult(Query query, Pageable pageable) { - query.with(pageable); + if (pageable.isUnpaged()) { + return query; + } + + Query target = Query.of(query); + target.skip(pageable.getOffset()); + target.limit(pageable.getPageSize() + 1); - return pageable.isPaged() ? query.limit(pageable.getPageSize() + 1) : query; + return target; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/util/SliceUtilsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/util/SliceUtilsUnitTests.java new file mode 100644 index 000000000..8dc952e8a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/util/SliceUtilsUnitTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 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.repository.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.util.stream.Stream; + +import org.bson.Document; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; + +/** + * Unit test for {@link SliceUtils}. + * + * @author Christoph Strobl + */ +class SliceUtilsUnitTests { + + @ParameterizedTest // GH-4889 + @MethodSource("paged") + void pagedPageableModifiesQuery(Pageable page) { + + Query source = new BasicQuery(Document.parse("{ 'spring' : 'data' }")); + + Query target = SliceUtils.limitResult(source, page); + + assertThat(target.getQueryObject()).isEqualTo(source.getQueryObject()); + assertThat(target).isNotSameAs(source); + assertThat(target.isLimited()).isTrue(); + assertThat(target.getSkip()).isEqualTo(page.getOffset()); + assertThat(target.getLimit()).isEqualTo(page.toLimit().max() + 1); + assertThat(target.getSortObject()).isEqualTo(source.getSortObject()); + } + + @ParameterizedTest // GH-4889 + @MethodSource("unpaged") + void unpagedPageableDoesNotModifyQuery(Pageable page) { + + Query source = spy(new BasicQuery(Document.parse("{ 'spring' : 'data' }"))); + + Query target = SliceUtils.limitResult(source, page); + + verifyNoInteractions(source); + + assertThat(target).isSameAs(source); + assertThat(target.isLimited()).isFalse(); + } + + public static Stream paged() { + return Stream.of(Arguments.of(Pageable.ofSize(1)), Arguments.of(PageRequest.of(0, 10)), + Arguments.of(PageRequest.of(0, 10, Direction.ASC, "name"))); + } + + public static Stream unpaged() { + return Stream.of(Arguments.of(Pageable.unpaged()), Arguments.of(Pageable.unpaged(Sort.by("name")))); + } +}