diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index 1d9c5cb8b..3712be956 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -19,6 +19,8 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; +import java.util.Arrays; +import java.util.Collection; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -31,6 +33,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.DeleteSpecification; import org.springframework.data.jpa.domain.PredicateSpecification; @@ -229,7 +232,7 @@ public interface JpaSpecificationExecutor { * @since 4.0 */ default R findBy(PredicateSpecification spec, - Function, R> queryFunction) { + Function, R> queryFunction) { return findBy(Specification.where(spec), queryFunction); } @@ -275,6 +278,21 @@ public interface JpaSpecificationExecutor { @Override SpecificationFluentQuery project(Collection properties); + /** + * Get a page of matching elements for {@link Pageable} and provide a custom {@link Specification count + * specification}. + * + * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be + * {@literal null}. The given {@link Pageable} will override any previously specified {@link Sort sort} if + * the {@link Sort} object is not {@link Sort#isUnsorted()}. Any potentially specified {@link #limit(int)} + * will be overridden by {@link Pageable#getPageSize()}. + * @param countSpec specification used to count results. + * @return + */ + default Page page(Pageable pageable, PredicateSpecification countSpec) { + return page(pageable, Specification.where(countSpec)); + } + /** * Get a page of matching elements for {@link Pageable} and provide a custom {@link Specification count * specification}. diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java index 68b4eb258..a1c91b914 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/FetchableFluentQueryBySpecification.java @@ -226,6 +226,26 @@ class FetchableFluentQueryBySpecification extends FluentQuerySupport private Slice readSlice(Pageable pageable) { + TypedQuery pagedQuery = createSortedAndProjectedQuery(); + + if (pageable.isPaged()) { + pagedQuery.setFirstResult(PageableUtils.getOffsetAsInteger(pageable)); + pagedQuery.setMaxResults(pageable.getPageSize() + 1); + } + + List resultList = pagedQuery.getResultList(); + boolean hasNext = resultList.size() > pageable.getPageSize(); + if (hasNext) { + resultList = resultList.subList(0, pageable.getPageSize()); + } + + List slice = convert(resultList); + + return new SliceImpl<>(slice, pageable, hasNext); + } + + private Slice readSlice(Pageable pageable, @Nullable Specification countSpec) { + TypedQuery pagedQuery = createSortedAndProjectedQuery(pageable.getSort()); if (pageable.isPaged()) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 3973ec98c..0e2c25a86 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -2800,6 +2800,44 @@ class UserRepositoryTests { assertThat(page0.getTotalElements()).isEqualTo(3L); } + @Test // GH-2274 + void findByFluentSpecificationSlice() { + + flushTestUsers(); + + Slice slice = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).slice(PageRequest.of(0, 2))); + + assertThat(slice).isNotInstanceOf(Page.class); + assertThat(slice.getContent()).containsExactly(thirdUser, firstUser); + assertThat(slice.hasNext()).isTrue(); + + slice = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).slice(PageRequest.of(0, 3))); + + assertThat(slice).isNotInstanceOf(Page.class); + assertThat(slice).hasSize(3); + assertThat(slice.hasNext()).isFalse(); + } + + @Test // GH-3727 + void findByFluentSpecificationPageCustomCountSpec() { + + flushTestUsers(); + + Page page0 = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2), (root, query, criteriaBuilder) -> null)); + + assertThat(page0.getContent()).containsExactly(thirdUser, firstUser); + assertThat(page0.getTotalElements()).isEqualTo(4L); + + page0 = repository.findBy(userHasFirstnameLike("v"), + q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2))); + + assertThat(page0.getContent()).containsExactly(thirdUser, firstUser); + assertThat(page0.getTotalElements()).isEqualTo(3L); + } + @Test // GH-2274, GH-3716 void findByFluentSpecificationWithInterfaceBasedProjection() {