Consider Sort override from Pageable using Fluent Query API.

We now consider properly a sort value from a given Pageable. Previously, the sort Parameter was not applied from Pageable but from a previous sort(…) call.

Also, we apply the given Sort to the resulting Pageable to ensure sort continuity.

Closes #3762
This commit is contained in:
Mark Paluch
2025-02-05 09:56:24 +01:00
parent 412e5eee28
commit baca9e1995
5 changed files with 91 additions and 20 deletions

View File

@@ -132,7 +132,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
@Override
public R oneValue() {
List<?> results = createSortedAndProjectedQuery() //
List<?> results = createSortedAndProjectedQuery(this.sort) //
.limit(2) // Never need more than 2 values
.fetch();
@@ -146,7 +146,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
@Override
public R firstValue() {
List<?> results = createSortedAndProjectedQuery() //
List<?> results = createSortedAndProjectedQuery(this.sort) //
.limit(1) // Never need more than 1 value
.fetch();
@@ -155,7 +155,11 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
@Override
public List<R> all() {
return convert(createSortedAndProjectedQuery().fetch());
return all(this.sort);
}
private List<R> all(Sort sort) {
return convert(createSortedAndProjectedQuery(sort).fetch());
}
@Override
@@ -168,13 +172,13 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
@Override
public Page<R> page(Pageable pageable) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSortOr(this.sort))) : readPage(pageable);
}
@Override
public Stream<R> stream() {
return createSortedAndProjectedQuery() //
return createSortedAndProjectedQuery(this.sort) //
.stream() //
.map(getConversionFunction());
}
@@ -189,7 +193,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
return existsOperation.apply(predicate);
}
private AbstractJPAQuery<?, ?> createSortedAndProjectedQuery() {
private AbstractJPAQuery<?, ?> createSortedAndProjectedQuery(Sort sort) {
AbstractJPAQuery<?, ?> query = finder.apply(sort);
@@ -206,6 +210,7 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
private Page<R> readPage(Pageable pageable) {
Sort sort = pageable.getSortOr(this.sort);
AbstractJPAQuery<?, ?> query = pagedFinder.apply(sort, pageable);
if (!properties.isEmpty()) {
@@ -214,7 +219,8 @@ class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<S, R> imp
List<R> paginatedResults = convert(query.fetch());
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(predicate));
return PageableExecutionUtils.getPage(paginatedResults, withSort(pageable, sort),
() -> countOperation.apply(predicate));
}
private List<R> convert(List<?> resultList) {

View File

@@ -89,8 +89,14 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
Assert.notNull(sort, "Sort must not be null");
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, this.sort.and(sort), limit,
properties, finder, scroll, countOperation, existsOperation, entityManager, projectionFactory);
Sort sort1 = this.sort.and(sort);
if (this.sort == sort1) {
return this;
}
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort1, limit, properties, finder,
scroll, countOperation, existsOperation, entityManager, projectionFactory);
}
@Override
@@ -98,8 +104,8 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
Assert.isTrue(limit >= 0, "Limit must not be negative");
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit,
properties, finder, scroll, countOperation, existsOperation, entityManager, projectionFactory);
return new FetchableFluentQueryBySpecification<>(spec, entityType, resultType, sort, limit, properties, finder,
scroll, countOperation, existsOperation, entityManager, projectionFactory);
}
@Override
@@ -124,7 +130,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
@Override
public R oneValue() {
List<?> results = createSortedAndProjectedQuery() //
List<?> results = createSortedAndProjectedQuery(this.sort) //
.setMaxResults(2) // Never need more than 2 values
.getResultList();
@@ -138,7 +144,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
@Override
public R firstValue() {
List<?> results = createSortedAndProjectedQuery() //
List<?> results = createSortedAndProjectedQuery(this.sort) //
.setMaxResults(1) // Never need more than 1 value
.getResultList();
@@ -147,7 +153,11 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
@Override
public List<R> all() {
return convert(createSortedAndProjectedQuery().getResultList());
return all(this.sort);
}
private List<R> all(Sort sort) {
return convert(createSortedAndProjectedQuery(sort).getResultList());
}
@Override
@@ -160,13 +170,13 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
@Override
public Page<R> page(Pageable pageable) {
return pageable.isUnpaged() ? new PageImpl<>(all()) : readPage(pageable);
return pageable.isUnpaged() ? new PageImpl<>(all(pageable.getSortOr(this.sort))) : readPage(pageable);
}
@Override
public Stream<R> stream() {
return createSortedAndProjectedQuery() //
return createSortedAndProjectedQuery(this.sort) //
.getResultStream() //
.map(getConversionFunction());
}
@@ -181,7 +191,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
return existsOperation.apply(spec);
}
private TypedQuery<S> createSortedAndProjectedQuery() {
private TypedQuery<S> createSortedAndProjectedQuery(Sort sort) {
TypedQuery<S> query = finder.apply(sort);
@@ -198,7 +208,8 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
private Page<R> readPage(Pageable pageable) {
TypedQuery<S> pagedQuery = createSortedAndProjectedQuery();
Sort sort = pageable.getSortOr(this.sort);
TypedQuery<S> pagedQuery = createSortedAndProjectedQuery(sort);
if (pageable.isPaged()) {
pagedQuery.setFirstResult(PageableUtils.getOffsetAsInteger(pageable));
@@ -207,7 +218,7 @@ class FetchableFluentQueryBySpecification<S, R> extends FluentQuerySupport<S, R>
List<R> paginatedResults = convert(pagedQuery.getResultList());
return PageableExecutionUtils.getPage(paginatedResults, pageable, () -> countOperation.apply(spec));
return PageableExecutionUtils.getPage(paginatedResults, withSort(pageable, sort), () -> countOperation.apply(spec));
}
private List<R> convert(List<S> resultList) {

View File

@@ -24,6 +24,8 @@ import java.util.Set;
import java.util.function.Function;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.projection.ProjectionFactory;
@@ -49,7 +51,7 @@ abstract class FluentQuerySupport<S, R> {
protected final ProjectionFactory projectionFactory;
FluentQuerySupport(Class<R> resultType, Sort sort, int limit, @Nullable Collection<String> properties,
Class<S> entityType, ProjectionFactory projectionFactory) {
Class<S> entityType, ProjectionFactory projectionFactory) {
this.resultType = resultType;
this.sort = sort;
@@ -87,6 +89,15 @@ abstract class FluentQuerySupport<S, R> {
return o -> DefaultConversionService.getSharedInstance().convert(o, targetType);
}
Pageable withSort(Pageable pageable, Sort sort) {
if (pageable instanceof PageRequest pr && pageable.getSort() != sort) {
return pr.withSort(sort);
}
return pageable;
}
interface ScrollQueryFactory {
Query createQuery(Sort sort, ScrollPosition scrollPosition);
}

View File

@@ -2434,6 +2434,24 @@ class UserRepositoryTests {
assertThat(page1.getContent()).containsExactly(fourthUser);
}
@Test // GH-3762
void findByFluentExamplePageSortOverride() {
flushTestUsers();
User prototype = new User();
prototype.setFirstname("v");
Example<User> userProbe = of(prototype, matching().withIgnorePaths("age", "createdAt", "active")
.withMatcher("firstname", GenericPropertyMatcher::contains));
Page<User> page = repository.findBy(userProbe, //
q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2, Sort.by(DESC, "firstname"))));
assertThat(page.getContent()).containsExactly(fourthUser, firstUser);
assertThat(repository.findAll(page.nextPageable())).containsExactly(secondUser, thirdUser);
}
@Test // GH-2294
void findByFluentExampleWithInterfaceBasedProjection() {
@@ -2689,6 +2707,18 @@ class UserRepositoryTests {
assertThat(page1.getContent()).containsExactly(fourthUser);
}
@Test // GH-3762
void findByFluentSpecificationSortOverridePage() {
flushTestUsers();
Page<User> page = repository.findBy(userHasFirstnameLike("v"),
q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 2, Sort.by(DESC, "firstname"))));
assertThat(page.getContent()).containsExactly(fourthUser, firstUser);
assertThat(repository.findAll(page.nextPageable())).containsExactly(secondUser, thirdUser);
}
@Test // GH-2274
void findByFluentSpecificationWithInterfaceBasedProjection() {

View File

@@ -399,6 +399,19 @@ class QuerydslJpaPredicateExecutorUnitTests {
assertThat(page1.getContent()).containsExactly(oliver);
}
@Test // GH-3762
void findByFluentPredicateSortOverridePage() {
Predicate predicate = user.firstname.contains("v");
Page<User> page = predicateExecutor.findBy(predicate,
q -> q.sortBy(Sort.by("firstname")).page(PageRequest.of(0, 1, Sort.by(Direction.DESC, "firstname"))));
assertThat(page.getContent()).containsOnly(oliver);
assertThat(predicateExecutor.findAll(predicate, page.nextPageable())).containsOnly(dave);
}
@Test // GH-2294
void findByFluentPredicateWithInterfaceBasedProjection() {