From 4fe4640f274b476383cf2cf7b5bd8bceac2cb304 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 5 Feb 2025 11:55:51 +0100 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index ded4d85d02..97ecdf60ad 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0-GH-4889-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 58c63dfc97..e35e85033a 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0-GH-4889-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 37e68c6f78..470fb51ad2 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0-GH-4889-SNAPSHOT ../pom.xml From 5826c4a64366860a8e1d82c5c0c2ab39fa706e91 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 5 Feb 2025 12:15:54 +0100 Subject: [PATCH 2/3] =?UTF-8?q?Add=20support=20for=20`findBy(=E2=80=A6)`?= =?UTF-8?q?=20using=20predicate=20that=20returns=20a=20`Slice`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #4889 --- .../QuerydslMongoPredicateExecutor.java | 9 +++ ...eactiveQuerydslMongoPredicateExecutor.java | 12 +++- .../ReactiveSpringDataMongodbQuery.java | 14 ++++ .../support/SimpleMongoRepository.java | 14 +++- .../SimpleReactiveMongoRepository.java | 8 +++ .../repository/support/SliceUtils.java | 65 +++++++++++++++++++ .../support/SpringDataMongodbQuery.java | 15 +++++ .../SimpleReactiveMongoRepositoryTests.java | 27 ++++++++ ...ongoPredicateExecutorIntegrationTests.java | 18 +++++ ...veQuerydslMongoPredicateExecutorTests.java | 25 ++++++- .../support/SimpleMongoRepositoryTests.java | 19 ++++++ 11 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SliceUtils.java 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 0da7b76596..ec845510ce 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 @@ -27,6 +27,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.MongoOperations; @@ -271,6 +272,14 @@ public Page page(Pageable pageable) { return createQuery().fetchPage(pageable); } + @Override + public Slice slice(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null"); + + return createQuery().fetchSlice(pageable); + } + @Override public Stream stream() { return createQuery().stream(); 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 bb0284f915..9a1482823f 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 @@ -24,11 +24,13 @@ import org.bson.Document; 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.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; @@ -239,6 +241,14 @@ public Mono> page(Pageable pageable) { return createQuery().fetchPage(pageable); } + @Override + public Mono> slice(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null"); + + return createQuery().fetchSlice(pageable); + } + @Override public Mono count() { return createQuery().fetchCount(); 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 c23ca5a979..042751d30e 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 @@ -27,6 +27,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveFindOperation; @@ -109,6 +110,19 @@ Mono> fetchPage(Pageable pageable) { return content.flatMap(it -> ReactivePageableExecutionUtils.getPage(it, pageable, fetchCount())); } + /** + * Fetch all matching query results as Slice. + * + * @return {@link Mono} emitting the requested Slice. + */ + Mono> fetchSlice(Pageable pageable) { + + Mono> content = createQuery().map(it -> SliceUtils.getQuery(it, pageable)) + .flatMapMany(it -> find.matching(it).all()).collectList(); + + return content.map(it -> SliceUtils.getSlice(it, pageable)); + } + /** * Fetch the one matching query result. * 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 ce6dcdd3a4..b62bc73f10 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 @@ -33,6 +33,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.ExecutableFindOperation; @@ -136,8 +137,7 @@ public boolean existsById(ID id) { Query query = getIdQuery(id); getReadPreference().ifPresent(query::withReadPreference); - return mongoOperations.exists(query, entityInformation.getJavaType(), - entityInformation.getCollectionName()); + return mongoOperations.exists(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @Override @@ -455,6 +455,16 @@ public Page page(Pageable pageable) { return PageableExecutionUtils.getPage(list, pageable, this::count); } + @Override + public Slice slice(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null"); + + List resultList = createQuery(q -> SliceUtils.getQuery(q, pageable)).all(); + + return SliceUtils.getSlice(resultList, pageable); + } + @Override public Stream stream() { return createQuery().stream(); 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 e8ff60de46..af7a5f57c6 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 @@ -37,6 +37,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.MongoTemplate; @@ -592,6 +593,13 @@ public Mono> page(Pageable pageable) { return items.flatMap(content -> ReactivePageableExecutionUtils.getPage(content, pageable, this.count())); } + @Override + public Mono> slice(Pageable pageable) { + + return createQuery(q -> SliceUtils.getQuery(q, pageable)).all().collectList() + .map(it -> SliceUtils.getSlice(it, pageable)); + } + @Override public Mono count() { return createQuery().count(); 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/support/SliceUtils.java new file mode 100644 index 0000000000..167875481d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SliceUtils.java @@ -0,0 +1,65 @@ +/* + * 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.support; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.mongodb.core.query.Query; + +/** + * Utility methods for {@link Slice} handling. + * + * @author Mark Paluch + * @since 4.5 + */ +class SliceUtils { + + /** + * Creates a {@link Slice} given {@link Pageable} and {@link List} of results. + * + * @param + * @param resultList + * @param pageable + * @return + */ + public static Slice getSlice(List resultList, Pageable pageable) { + + boolean hasNext = resultList.size() > pageable.getPageSize(); + + if (hasNext) { + resultList = resultList.subList(0, pageable.getPageSize()); + } + + return new SliceImpl<>(resultList, pageable, hasNext); + } + + /** + * Customize query for slice retrieval. + * + * @param query + * @param pageable + * @return + */ + public static Query getQuery(Query query, Pageable pageable) { + + query.with(pageable); + + return pageable.isPaged() ? query.limit(pageable.getPageSize() + 1) : query; + } +} 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 c8cad08c09..cc5f44de23 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 @@ -22,10 +22,12 @@ import java.util.stream.Stream; import org.bson.Document; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.MongoOperations; @@ -182,6 +184,19 @@ public Page fetchPage(Pageable pageable) { } } + /** + * Fetch a {@link Slice}. + * + * @param pageable + * @return + */ + public Slice fetchSlice(Pageable pageable) { + + List content = find.matching(SliceUtils.getQuery(createQuery(), pageable)).all(); + + return SliceUtils.getSlice(content, pageable); + } + @Override public T fetchFirst() { try { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java index 8ab45e5964..44235c54ef 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java @@ -607,6 +607,33 @@ void findByShouldApplyPagination() { }).verifyComplete(); } + @Test // GH-4889 + void findByShouldApplySlice() { + + ReactivePerson probe = new ReactivePerson(); + probe.setLastname(oliver.getLastname()); + + repository + .findBy(Example.of(probe, matching().withIgnorePaths("age")), + it -> it.slice(PageRequest.of(0, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.hasNext()).isTrue(); + assertThat(it.getContent()).contains(dave); + }).verifyComplete(); + + repository + .findBy(Example.of(probe, matching().withIgnorePaths("age")), + it -> it.slice(PageRequest.of(1, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.hasNext()).isFalse(); + assertThat(it.getContent()).contains(oliver); + }).verifyComplete(); + } + @Test // GH-3757 void findByShouldCount() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java index 18d86d02dc..7d9024e2fb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.MongoDatabaseFactory; @@ -314,6 +315,7 @@ public void findByShouldApplyPagination() { Page first = repository.findBy(person.lastname.eq(oliver.getLastname()), it -> it.page(PageRequest.of(0, 1, Sort.by("firstname")))); + assertThat(first.getTotalElements()).isEqualTo(2); assertThat(first.getContent()).contains(dave); @@ -324,6 +326,22 @@ public void findByShouldApplyPagination() { assertThat(next.getContent()).contains(oliver); } + @Test // GH-4889 + public void findByShouldApplySlice() { + + Slice first = repository.findBy(person.lastname.eq(oliver.getLastname()), + it -> it.slice(PageRequest.of(0, 1, Sort.by("firstname")))); + + assertThat(first.hasNext()).isTrue(); + assertThat(first.getContent()).contains(dave); + + Slice next = repository.findBy(person.lastname.eq(oliver.getLastname()), + it -> it.slice(PageRequest.of(1, 1, Sort.by("firstname")))); + + assertThat(next.hasNext()).isFalse(); + assertThat(next.getContent()).contains(oliver); + } + @Test // GH-3757 public void findByShouldCount() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java index 7e6f6e4995..807b7aec22 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java @@ -31,6 +31,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -50,8 +51,8 @@ import org.springframework.data.mongodb.repository.User; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.mongodb.test.util.MongoTestUtils; -import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.mongodb.test.util.ReactiveMongoClientClosingTestConfiguration; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -414,6 +415,28 @@ public void findByShouldApplyPagination() { }).verifyComplete(); } + @Test // GH-4889 + public void findByShouldApplySlice() { + + repository + .findBy(person.lastname.eq(oliver.getLastname()), it -> it.slice(PageRequest.of(0, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.hasNext()).isTrue(); + assertThat(it.getContent()).containsOnly(dave); + }).verifyComplete(); + + repository + .findBy(person.lastname.eq(oliver.getLastname()), it -> it.slice(PageRequest.of(1, 1, Sort.by("firstname")))) // + .as(StepVerifier::create) // + .assertNext(it -> { + + assertThat(it.hasNext()).isFalse(); + assertThat(it.getContent()).containsOnly(oliver); + }).verifyComplete(); + } + @Test // GH-3757 public void findByShouldCount() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 98cbab868b..60c02ee775 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -38,6 +38,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.MongoTransactionManager; @@ -579,6 +580,24 @@ void findByShouldApplyPagination() { assertThat(next.getContent()).contains(oliver); } + @Test // GH-4889 + void findByShouldApplySlice() { + + Person probe = new Person(); + probe.setLastname(oliver.getLastname()); + + Slice first = repository.findBy(Example.of(probe, getMatcher()), + it -> it.slice(PageRequest.of(0, 1, Sort.by("firstname")))); + assertThat(first.hasNext()).isTrue(); + assertThat(first.getContent()).contains(dave); + + Slice next = repository.findBy(Example.of(probe, getMatcher()), + it -> it.slice(PageRequest.of(1, 1, Sort.by("firstname")))); + + assertThat(next.hasNext()).isFalse(); + assertThat(next.getContent()).contains(oliver); + } + @Test // GH-3757 void findByShouldCount() { From 551bb4f040ab7a5d676d5566e5a1d1f2481f72d6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 11 Feb 2025 14:31:01 +0100 Subject: [PATCH 3/3] Move and reuse SliceUtils --- .../repository/query/MongoQueryExecution.java | 3 +- .../ReactiveSpringDataMongodbQuery.java | 6 +- .../support/SimpleMongoRepository.java | 5 +- .../SimpleReactiveMongoRepository.java | 5 +- .../support/SpringDataMongodbQuery.java | 10 ++- .../{support => util}/SliceUtils.java | 25 +++--- .../repository/util/SliceUtilsUnitTests.java | 77 +++++++++++++++++++ 7 files changed, 111 insertions(+), 20 deletions(-) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/{support => util}/SliceUtils.java (67%) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/util/SliceUtilsUnitTests.java 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 cebdf4e408..dd2b78de59 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.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 @@ public Object execute(Query query) { 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 042751d30e..bd4b241837 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; @@ -114,13 +115,14 @@ Mono> fetchPage(Pageable pageable) { * Fetch all matching query results as Slice. * * @return {@link Mono} emitting the requested Slice. + * @since 4.5 */ Mono> 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 b62bc73f10..2f4c30ee7a 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.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 Slice slice(Pageable pageable) { 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 af7a5f57c6..1c1df2c9a1 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 @@ 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 Mono> page(Pageable pageable) { @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 cc5f44de23..0ef6c38744 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.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 Page fetchPage(Pageable pageable) { /** * Fetch a {@link Slice}. * - * @param pageable - * @return + * @param pageable defines range and sort of requested slice + * @return new instance of {@link Slice} containing matching results within range. + * @since 4.5 */ public Slice 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 67% 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 167875481d..db22c3fea4 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; @@ -28,7 +28,7 @@ * @author Mark Paluch * @since 4.5 */ -class SliceUtils { +public class SliceUtils { /** * Creates a {@link Slice} given {@link Pageable} and {@link List} of results. @@ -38,7 +38,7 @@ class SliceUtils { * @param pageable * @return */ - public static Slice getSlice(List resultList, Pageable pageable) { + public static Slice sliceResult(List resultList, Pageable pageable) { boolean hasNext = resultList.size() > pageable.getPageSize(); @@ -52,14 +52,21 @@ public static Slice getSlice(List resultList, Pageable pageable) { /** * Customize query for slice retrieval. * - * @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} or {@link Pageable#getSort() + * sorted}, the source query otherwise. */ - public static Query getQuery(Query query, Pageable pageable) { + public static Query limitResult(Query query, Pageable pageable) { + + if (pageable.isUnpaged()) { + return query; + } - query.with(pageable); + 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 0000000000..eb1353cde3 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/util/SliceUtilsUnitTests.java @@ -0,0 +1,77 @@ +/* + * 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; + +/** + * @author Christoph Strobl + */ +class SliceUtilsUnitTests { + + @ParameterizedTest + @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 + @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")))); + } +}