From 9f03598c900d6e390b04d1b5b5737f6fb7b9d443 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 23 Jan 2025 11:26:38 +0100 Subject: [PATCH] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SpecificationFluentQuery to include specification-related overloads. Also, add slice(…) terminal method to obtain a slice only without running a count query. See #3727 --- .gitignore | 1 + .../repository/JpaSpecificationExecutor.java | 57 ++++++++++++++++++- .../FetchableFluentQueryBySpecification.java | 51 ++++++++++++++--- .../support/SimpleJpaRepository.java | 9 +-- .../jpa/repository/UserRepositoryTests.java | 38 +++++++++++++ 5 files changed, 143 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 6306c81ec6..2cca7fefeb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ package-lock.json node build/ .mvn/.develocity +spring-data-jpa/gen 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 96438997d3..cacc8dfef3 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.CriteriaQuery; import jakarta.persistence.criteria.Root; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -26,6 +28,7 @@ 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.Specification; import org.springframework.data.repository.query.FluentQuery; @@ -84,6 +87,7 @@ public interface JpaSpecificationExecutor { * be counted. * @param pageable must not be {@literal null}. * @return never {@literal null}. + * @since 3.5 */ Page findAll(@Nullable Specification spec, @Nullable Specification countSpec, Pageable pageable); @@ -150,6 +154,57 @@ public interface JpaSpecificationExecutor { * @since 3.0 * @throws InvalidDataAccessApiUsageException if the query function returns the {@link FluentQuery} instance. */ - R findBy(Specification spec, Function, R> queryFunction); + R findBy(Specification spec, Function, R> queryFunction); + + /** + * Extension to {@link FetchableFluentQuery} allowing slice results and pagination with a custom count + * {@link Specification}. + * + * @param + * @since 3.5 + */ + interface SpecificationFluentQuery extends FluentQuery.FetchableFluentQuery { + + @Override + SpecificationFluentQuery sortBy(Sort sort); + + @Override + SpecificationFluentQuery limit(int limit); + + @Override + SpecificationFluentQuery as(Class resultType); + + @Override + default SpecificationFluentQuery project(String... properties) { + return this.project(Arrays.asList(properties)); + } + + @Override + SpecificationFluentQuery project(Collection properties); + + /** + * Get a slice of matching elements for {@link Pageable} by requesting {@code Pageable#getPageSize() + 1} elements. + * + * @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()}. + * @return + */ + Slice slice(Pageable pageable); + + /** + * 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 + */ + Page page(Pageable pageable, Specification countSpec); + } } 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 dab32519d1..12ed69ee4d 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 @@ -31,14 +31,18 @@ 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.SliceImpl; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor.SpecificationFluentQuery; import org.springframework.data.jpa.repository.query.ScrollDelegate; import org.springframework.data.jpa.support.PageableUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -52,7 +56,7 @@ * @since 3.0 */ class FetchableFluentQueryBySpecification extends FluentQuerySupport - implements FluentQuery.FetchableFluentQuery { + implements FluentQuery.FetchableFluentQuery, SpecificationFluentQuery { private final Specification spec; private final Function, TypedQuery> finder; @@ -85,7 +89,7 @@ private FetchableFluentQueryBySpecification(Specification spec, Class enti } @Override - public FetchableFluentQuery sortBy(Sort sort) { + public SpecificationFluentQuery sortBy(Sort sort) { Assert.notNull(sort, "Sort must not be null"); @@ -94,7 +98,7 @@ public FetchableFluentQuery sortBy(Sort sort) { } @Override - public FetchableFluentQuery limit(int limit) { + public SpecificationFluentQuery limit(int limit) { Assert.isTrue(limit >= 0, "Limit must not be negative"); @@ -103,7 +107,7 @@ public FetchableFluentQuery limit(int limit) { } @Override - public FetchableFluentQuery as(Class resultType) { + public SpecificationFluentQuery as(Class resultType) { Assert.notNull(resultType, "Projection target type must not be null"); @@ -112,7 +116,7 @@ public FetchableFluentQuery as(Class resultType) { } @Override - public FetchableFluentQuery project(Collection properties) { + public SpecificationFluentQuery project(Collection properties) { return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder, scroll, countOperation, existsOperation, entityManager, projectionFactory); @@ -155,9 +159,20 @@ public Window scroll(ScrollPosition scrollPosition) { return scroll.scroll(this, scrollPosition).map(getConversionFunction()); } + @Override + public Slice slice(Pageable pageable) { + return pageable.isUnpaged() ? new PageImpl<>(all()) : readSlice(pageable); + } + @Override public Page page(Pageable pageable) { - return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable); + return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable, spec); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Page page(Pageable pageable, Specification countSpec) { + return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable, (Specification) countSpec); } @Override @@ -193,7 +208,27 @@ private TypedQuery createSortedAndProjectedQuery() { return query; } - private Page readPage(Pageable pageable) { + 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 Page readPage(Pageable pageable, @Nullable Specification countSpec) { TypedQuery pagedQuery = createSortedAndProjectedQuery(); @@ -204,7 +239,7 @@ private Page readPage(Pageable pageable) { List paginatedResults = convert(pagedQuery.getResultList()); - return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(spec)); + return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(countSpec)); } private List convert(List resultList) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 0e3bf0c115..e854c5219f 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -506,7 +506,8 @@ public long delete(@Nullable Specification spec) { } @Override - public R findBy(Specification spec, Function, R> queryFunction) { + public R findBy(Specification spec, + Function, R> queryFunction) { Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL); Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL); @@ -515,7 +516,7 @@ public R findBy(Specification spec, Function R doFindBy(Specification spec, Class domainClass, - Function, R> queryFunction) { + Function, R> queryFunction) { Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL); Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL); @@ -550,7 +551,7 @@ private R doFindBy(Specification spec, Class domainClass, FetchableFluentQueryBySpecification fluentQuery = new FetchableFluentQueryBySpecification<>(spec, domainClass, finder, scrollDelegate, this::count, this::exists, this.entityManager, getProjectionFactory()); - R result = queryFunction.apply((FetchableFluentQuery) fluentQuery); + R result = queryFunction.apply((SpecificationFluentQuery) fluentQuery); if (result instanceof FluentQuery) { throw new InvalidDataAccessApiUsageException( @@ -718,7 +719,7 @@ protected Page readPage(TypedQuery query, Pageable pageable, @Nullable Spe * @param spec can be {@literal null}. * @param pageable can be {@literal null}. */ - protected Page readPage(TypedQuery query, final Class domainClass, Pageable pageable, + protected Page readPage(TypedQuery query, Class domainClass, Pageable pageable, @Nullable Specification spec) { 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 6477d20c3e..b04d6c7e03 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 @@ -2706,6 +2706,44 @@ void findByFluentSpecificationPage() { assertThat(page1.getContent()).containsExactly(fourthUser); } + @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() {