Skip to content

Commit

Permalink
Polishing.
Browse files Browse the repository at this point in the history
Add SpecificationFluentQuery to include specification-related overloads. Also, add slice(…) terminal method to obtain a slice only without running a count query.

See #3727
  • Loading branch information
mp911de committed Jan 23, 2025
1 parent 5315848 commit 9f03598
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ package-lock.json
node
build/
.mvn/.develocity
spring-data-jpa/gen
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
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;

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;
Expand Down Expand Up @@ -84,6 +87,7 @@ public interface JpaSpecificationExecutor<T> {
* be counted.
* @param pageable must not be {@literal null}.
* @return never {@literal null}.
* @since 3.5
*/
Page<T> findAll(@Nullable Specification<T> spec, @Nullable Specification<T> countSpec, Pageable pageable);

Expand Down Expand Up @@ -150,6 +154,57 @@ public interface JpaSpecificationExecutor<T> {
* @since 3.0
* @throws InvalidDataAccessApiUsageException if the query function returns the {@link FluentQuery} instance.
*/
<S extends T, R> R findBy(Specification<T> spec, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
<S extends T, R> R findBy(Specification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction);

/**
* Extension to {@link FetchableFluentQuery} allowing slice results and pagination with a custom count
* {@link Specification}.
*
* @param <T>
* @since 3.5
*/
interface SpecificationFluentQuery<T> extends FluentQuery.FetchableFluentQuery<T> {

@Override
SpecificationFluentQuery<T> sortBy(Sort sort);

@Override
SpecificationFluentQuery<T> limit(int limit);

@Override
<R> SpecificationFluentQuery<R> as(Class<R> resultType);

@Override
default SpecificationFluentQuery<T> project(String... properties) {
return this.project(Arrays.asList(properties));
}

@Override
SpecificationFluentQuery<T> project(Collection<String> 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<T> 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<T> page(Pageable pageable, Specification<?> countSpec);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -52,7 +56,7 @@
* @since 3.0
*/
class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
implements FluentQuery.FetchableFluentQuery<R> {
implements FluentQuery.FetchableFluentQuery<R>, SpecificationFluentQuery<R> {

private final Specification<S> spec;
private final Function<FluentQuerySupport<?, ?>, TypedQuery<S>> finder;
Expand Down Expand Up @@ -85,7 +89,7 @@ private FetchableFluentQueryBySpecification(Specification<S> spec, Class<S> enti
}

@Override
public FetchableFluentQuery<R> sortBy(Sort sort) {
public SpecificationFluentQuery<R> sortBy(Sort sort) {

Assert.notNull(sort, "Sort must not be null");

Expand All @@ -94,7 +98,7 @@ public FetchableFluentQuery<R> sortBy(Sort sort) {
}

@Override
public FetchableFluentQuery<R> limit(int limit) {
public SpecificationFluentQuery<R> limit(int limit) {

Assert.isTrue(limit >= 0, "Limit must not be negative");

Expand All @@ -103,7 +107,7 @@ public FetchableFluentQuery<R> limit(int limit) {
}

@Override
public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
public <NR> SpecificationFluentQuery<NR> as(Class<NR> resultType) {

Assert.notNull(resultType, "Projection target type must not be null");

Expand All @@ -112,7 +116,7 @@ public <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
}

@Override
public FetchableFluentQuery<R> project(Collection<String> properties) {
public SpecificationFluentQuery<R> project(Collection<String> properties) {

return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
scroll, countOperation, existsOperation, entityManager, projectionFactory);
Expand Down Expand Up @@ -155,9 +159,20 @@ public Window<R> scroll(ScrollPosition scrollPosition) {
return scroll.scroll(this, scrollPosition).map(getConversionFunction());
}

@Override
public Slice<R> slice(Pageable pageable) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readSlice(pageable);
}

@Override
public Page<R> 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<R> page(Pageable pageable, Specification<?> countSpec) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable, (Specification) countSpec);
}

@Override
Expand Down Expand Up @@ -193,7 +208,27 @@ private TypedQuery<S> createSortedAndProjectedQuery() {
return query;
}

private Page<R> readPage(Pageable pageable) {
private Slice<R> readSlice(Pageable pageable) {

TypedQuery<S> pagedQuery = createSortedAndProjectedQuery();

if (pageable.isPaged()) {
pagedQuery.setFirstResult(PageableUtils.getOffsetAsInteger(pageable));
pagedQuery.setMaxResults(pageable.getPageSize() + 1);
}

List<S> resultList = pagedQuery.getResultList();
boolean hasNext = resultList.size() > pageable.getPageSize();
if (hasNext) {
resultList = resultList.subList(0, pageable.getPageSize());
}

List<R> slice = convert(resultList);

return new SliceImpl<>(slice, pageable, hasNext);
}

private Page<R> readPage(Pageable pageable, @Nullable Specification<S> countSpec) {

TypedQuery<S> pagedQuery = createSortedAndProjectedQuery();

Expand All @@ -204,7 +239,7 @@ private Page<R> readPage(Pageable pageable) {

List<R> paginatedResults = convert(pagedQuery.getResultList());

return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(spec));
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(countSpec));
}

private List<R> convert(List<S> resultList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,8 @@ public long delete(@Nullable Specification<T> spec) {
}

@Override
public <S extends T, R> R findBy(Specification<T> spec, Function<FetchableFluentQuery<S>, R> queryFunction) {
public <S extends T, R> R findBy(Specification<T> spec,
Function<? super SpecificationFluentQuery<S>, R> queryFunction) {

Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL);
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);
Expand All @@ -515,7 +516,7 @@ public <S extends T, R> R findBy(Specification<T> spec, Function<FetchableFluent
}

private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass,
Function<FetchableFluentQuery<S>, R> queryFunction) {
Function<? super SpecificationFluentQuery<S>, R> queryFunction) {

Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL);
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);
Expand Down Expand Up @@ -550,7 +551,7 @@ private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass,
FetchableFluentQueryBySpecification<?, T> fluentQuery = new FetchableFluentQueryBySpecification<>(spec, domainClass,
finder, scrollDelegate, this::count, this::exists, this.entityManager, getProjectionFactory());

R result = queryFunction.apply((FetchableFluentQuery<S>) fluentQuery);
R result = queryFunction.apply((SpecificationFluentQuery<S>) fluentQuery);

if (result instanceof FluentQuery<?>) {
throw new InvalidDataAccessApiUsageException(
Expand Down Expand Up @@ -718,7 +719,7 @@ protected Page<T> readPage(TypedQuery<T> query, Pageable pageable, @Nullable Spe
* @param spec can be {@literal null}.
* @param pageable can be {@literal null}.
*/
protected <S extends T> Page<S> readPage(TypedQuery<S> query, final Class<S> domainClass, Pageable pageable,
protected <S extends T> Page<S> readPage(TypedQuery<S> query, Class<S> domainClass, Pageable pageable,
@Nullable Specification<S> spec) {

if (pageable.isPaged()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2706,6 +2706,44 @@ void findByFluentSpecificationPage() {
assertThat(page1.getContent()).containsExactly(fourthUser);
}

@Test // GH-2274
void findByFluentSpecificationSlice() {

flushTestUsers();

Slice<User> 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<User> 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() {

Expand Down

0 comments on commit 9f03598

Please sign in to comment.