HHH-17779 previous-page nativation for key-based pagination
This commit is contained in:
parent
1eff3c990b
commit
5d498c1063
|
@ -12,6 +12,9 @@ import org.hibernate.Internal;
|
|||
import java.util.List;
|
||||
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_FIRST_ON_NEXT_PAGE;
|
||||
import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_LAST_ON_PREVIOUS_PAGE;
|
||||
import static org.hibernate.query.KeyedPage.KeyInterpretation.NO_KEY;
|
||||
|
||||
/**
|
||||
* Support for pagination based on a unique key of the result
|
||||
|
@ -49,15 +52,17 @@ public class KeyedPage<R> {
|
|||
private final List<Order<? super R>> keyDefinition;
|
||||
private final Page page;
|
||||
private final List<Comparable<?>> key;
|
||||
private final KeyInterpretation keyInterpretation;
|
||||
|
||||
KeyedPage(List<Order<? super R>> keyDefinition, Page page) {
|
||||
this( keyDefinition, page, null );
|
||||
this( keyDefinition, page, null, NO_KEY );
|
||||
}
|
||||
|
||||
KeyedPage(List<Order<? super R>> keyDefinition, Page page, List<Comparable<?>> key) {
|
||||
this.page = page;
|
||||
KeyedPage(List<Order<? super R>> keyDefinition, Page page, List<Comparable<?>> key, KeyInterpretation interpretation) {
|
||||
this.keyDefinition = unmodifiableList(keyDefinition);
|
||||
this.page = page;
|
||||
this.key = key;
|
||||
this.keyInterpretation = interpretation;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,36 +83,76 @@ public class KeyedPage<R> {
|
|||
}
|
||||
|
||||
/**
|
||||
* The key of the last result on the previous page, which is used
|
||||
* to locate the start of the current page.
|
||||
* The key of the last result on the previous page, or of the
|
||||
* first result on the next page, which may be used to locate
|
||||
* the start or end, respectively, of the current page.
|
||||
* <p>
|
||||
* A null key indicates that an {@linkplain Page#getFirstResult()
|
||||
* offset} should be used instead. This is used to obtain an
|
||||
* initial page of results.
|
||||
*
|
||||
* @return the key of the last result on the previous page, or
|
||||
* null if an offset should be used to obtain an initial
|
||||
* page
|
||||
* @return the key, or null if an offset should be used
|
||||
*/
|
||||
public List<Comparable<?>> getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the {@link #getKey() key} should be
|
||||
* interpreted as the last result on the previous page, or
|
||||
* as the first result on the next page.
|
||||
*/
|
||||
public KeyInterpretation getKeyInterpretation() {
|
||||
return keyInterpretation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a specification of the next page of results, which is
|
||||
* to be located using the given key, which must be the key of
|
||||
* the last result on this page.
|
||||
*
|
||||
* @param keyOfLastResult the key of the last result on this page
|
||||
* @param keyOfLastResultOnThisPage the key of the last result on this page
|
||||
* @return a {@link KeyedPage} representing the next page of results
|
||||
*/
|
||||
@Internal
|
||||
public KeyedPage<R> nextPage(List<Comparable<?>> keyOfLastResult) {
|
||||
return new KeyedPage<>( keyDefinition, page.next(), keyOfLastResult );
|
||||
public KeyedPage<R> nextPage(List<Comparable<?>> keyOfLastResultOnThisPage) {
|
||||
return new KeyedPage<>( keyDefinition, page.next(), keyOfLastResultOnThisPage, KEY_OF_LAST_ON_PREVIOUS_PAGE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a specification of the previous page of results, which
|
||||
* is to be located using the given key, which must be the key of
|
||||
* the first result on this page.
|
||||
*
|
||||
* @param keyOfFirstResultOnThisPage the key of the first result on this page
|
||||
* @return a {@link KeyedPage} representing the next page of results
|
||||
*/
|
||||
@Internal
|
||||
public KeyedPage<R> withKey(List<Comparable<?>> keyOfLastResult) {
|
||||
return new KeyedPage<>( keyDefinition, page, keyOfLastResult );
|
||||
public KeyedPage<R> previousPage(List<Comparable<?>> keyOfFirstResultOnThisPage) {
|
||||
if ( page.isFirst() ) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return new KeyedPage<>( keyDefinition, page.previous(), keyOfFirstResultOnThisPage, KEY_OF_FIRST_ON_NEXT_PAGE );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the given key to the specification of this page,
|
||||
* with the given interpretation.
|
||||
*
|
||||
* @return a {@link KeyedPage} representing the same page
|
||||
* of results, but which may be located using the
|
||||
* given key
|
||||
*/
|
||||
@Internal
|
||||
public KeyedPage<R> withKey(List<Comparable<?>> key, KeyInterpretation interpretation) {
|
||||
return new KeyedPage<>( keyDefinition, page, key, interpretation );
|
||||
}
|
||||
|
||||
public enum KeyInterpretation {
|
||||
KEY_OF_LAST_ON_PREVIOUS_PAGE,
|
||||
KEY_OF_FIRST_ON_NEXT_PAGE,
|
||||
NO_KEY
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,12 +30,16 @@ public class KeyedResultList<R> {
|
|||
private final List<List<?>> keyList;
|
||||
private final KeyedPage<R> page;
|
||||
private final KeyedPage<R> nextPage;
|
||||
private final KeyedPage<R> previousPage;
|
||||
|
||||
public KeyedResultList(List<R> resultList, List<List<?>> keyList, KeyedPage<R> page, KeyedPage<R> nextPage) {
|
||||
public KeyedResultList(
|
||||
List<R> resultList, List<List<?>> keyList,
|
||||
KeyedPage<R> page, KeyedPage<R> nextPage, KeyedPage<R> previousPage) {
|
||||
this.resultList = resultList;
|
||||
this.keyList = keyList;
|
||||
this.page = page;
|
||||
this.nextPage = nextPage;
|
||||
this.previousPage = previousPage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,6 +75,15 @@ public class KeyedResultList<R> {
|
|||
return nextPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* The specification of the previous page of results,
|
||||
* or {@code null} if it is known that this is the
|
||||
* first page.
|
||||
*/
|
||||
public KeyedPage<R> getPreviousPage() {
|
||||
return previousPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if this is known to be the
|
||||
* last page of results.
|
||||
|
|
|
@ -9,8 +9,11 @@ package org.hibernate.query;
|
|||
import jakarta.persistence.metamodel.SingularAttribute;
|
||||
import org.hibernate.Incubating;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
/**
|
||||
* A rule for sorting a query result set.
|
||||
* <p>
|
||||
|
@ -92,6 +95,16 @@ public class Order<X> {
|
|||
this.ignoreCase = ignoreCase;
|
||||
}
|
||||
|
||||
private Order(Order<X> other, SortDirection order) {
|
||||
this.order = order;
|
||||
this.attribute = other.attribute;
|
||||
this.entityClass = other.entityClass;
|
||||
this.attributeName = other.attributeName;
|
||||
this.nullPrecedence = other.nullPrecedence;
|
||||
this.element = other.element;
|
||||
this.ignoreCase = other.ignoreCase;
|
||||
}
|
||||
|
||||
public static <T> Order<T> asc(SingularAttribute<T,?> attribute) {
|
||||
return new Order<>(SortDirection.ASCENDING, NullPrecedence.NONE, attribute);
|
||||
}
|
||||
|
@ -176,6 +189,10 @@ public class Order<X> {
|
|||
return element;
|
||||
}
|
||||
|
||||
public Order<X> reverse() {
|
||||
return new Order<>( this, order.reverse() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return attributeName + " " + order;
|
||||
|
@ -200,4 +217,18 @@ public class Order<X> {
|
|||
public int hashCode() {
|
||||
return Objects.hash( order, element, attributeName, entityClass );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the direction of the given ordering list
|
||||
*
|
||||
* @param ordering a list of {@code Order} items
|
||||
* @return a new list, with each {@code Order} reversed
|
||||
*
|
||||
* @see #reverse()
|
||||
*
|
||||
* @since 6.5
|
||||
*/
|
||||
public static <T> List<Order<? super T>> reverse(List<Order<? super T>> ordering) {
|
||||
return ordering.stream().map(Order::reverse).collect(toList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,9 @@ public class Page {
|
|||
}
|
||||
|
||||
public <R> KeyedPage<R> keyedBy(Order<? super R> keyDefinition) {
|
||||
if ( keyDefinition == null ) {
|
||||
throw new IllegalArgumentException("Key definition must not null");
|
||||
}
|
||||
return new KeyedPage<>( List.of(keyDefinition), this );
|
||||
}
|
||||
|
||||
|
@ -98,8 +101,8 @@ public class Page {
|
|||
* @return a {@link KeyedPage} representing this page
|
||||
*/
|
||||
public <R> KeyedPage<R> keyedBy(List<Order<? super R>> keyDefinition) {
|
||||
if ( keyDefinition.isEmpty() ) {
|
||||
throw new IllegalArgumentException("Key definition must not be empty");
|
||||
if ( keyDefinition == null || keyDefinition.isEmpty() ) {
|
||||
throw new IllegalArgumentException("Key definition must not be empty or null");
|
||||
}
|
||||
return new KeyedPage<>( keyDefinition, this );
|
||||
}
|
||||
|
|
|
@ -27,11 +27,11 @@ import org.hibernate.query.sqm.tree.SqmStatement;
|
|||
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
|
||||
import org.hibernate.sql.results.internal.TupleMetadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.hibernate.cfg.QuerySettings.FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH;
|
||||
import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_FIRST_ON_NEXT_PAGE;
|
||||
import static org.hibernate.query.sqm.internal.KeyBasedPagination.paginate;
|
||||
import static org.hibernate.query.sqm.internal.KeyedResult.collectKeys;
|
||||
import static org.hibernate.query.sqm.internal.KeyedResult.collectResults;
|
||||
|
@ -152,13 +152,15 @@ abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
|
|||
@Override
|
||||
public KeyedResultList<R> getKeyedResultList(KeyedPage<R> keyedPage) {
|
||||
if ( keyedPage == null ) {
|
||||
throw new IllegalStateException( "KeyedPage was not null" );
|
||||
throw new IllegalArgumentException( "KeyedPage was null" );
|
||||
}
|
||||
final Page page = keyedPage.getPage();
|
||||
final List<Order<? super R>> keyDefinition = keyedPage.getKeyDefinition();
|
||||
final List<Comparable<?>> key = keyedPage.getKey();
|
||||
final List<Order<? super R>> keyDefinition = keyedPage.getKeyDefinition();
|
||||
final List<Order<? super R>> appliedKeyDefinition =
|
||||
keyedPage.getKeyInterpretation() == KEY_OF_FIRST_ON_NEXT_PAGE
|
||||
? Order.reverse(keyDefinition) : keyDefinition;
|
||||
|
||||
setOrder( keyDefinition );
|
||||
setMaxResults( page.getMaxResults() + 1 );
|
||||
if ( key == null ) {
|
||||
setFirstResult( page.getFirstResult() );
|
||||
|
@ -166,21 +168,28 @@ abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
|
|||
|
||||
// getQueryOptions().setQueryPlanCachingEnabled( false );
|
||||
final List<KeyedResult<R>> results =
|
||||
buildConcreteQueryPlan( paginateQuery( keyDefinition, key ), getQueryOptions() )
|
||||
buildConcreteQueryPlan( paginateQuery( appliedKeyDefinition, key ), getQueryOptions() )
|
||||
.performList(this);
|
||||
|
||||
return new KeyedResultList<>(
|
||||
collectResults( results, page.getSize() ),
|
||||
collectKeys( results, page.getSize() ),
|
||||
keyedPage,
|
||||
getNextPage( keyedPage, results )
|
||||
nextPage( keyedPage, results ),
|
||||
previousPage( keyedPage, results )
|
||||
);
|
||||
}
|
||||
|
||||
private static <R> KeyedPage<R> getNextPage(KeyedPage<R> keyedPage, List<KeyedResult<R>> executed) {
|
||||
private static <R> KeyedPage<R> nextPage(KeyedPage<R> keyedPage, List<KeyedResult<R>> results) {
|
||||
final int pageSize = keyedPage.getPage().getSize();
|
||||
return executed.size() == pageSize + 1
|
||||
? keyedPage.nextPage( executed.get(pageSize - 1).getKey() )
|
||||
return results.size() == pageSize + 1
|
||||
? keyedPage.nextPage( results.get(pageSize - 1).getKey() )
|
||||
: null;
|
||||
}
|
||||
|
||||
private static <R> KeyedPage<R> previousPage(KeyedPage<R> keyedPage, List<KeyedResult<R>> results) {
|
||||
return !results.isEmpty()
|
||||
? keyedPage.previousPage( results.get(0).getKey() )
|
||||
: null;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.hibernate.query.sqm.internal.SqmUtil.sortSpecification;
|
||||
|
||||
/**
|
||||
* Manipulation of SQM query tree for key-based pagination.
|
||||
|
@ -35,20 +37,22 @@ public class KeyBasedPagination {
|
|||
|
||||
static <R> SqmSelectStatement<KeyedResult<R>> paginate(
|
||||
List<Order<? super R>> keyDefinition, List<Comparable<?>> keyValues,
|
||||
SqmSelectStatement<KeyedResult<R>> sqm, NodeBuilder builder) {
|
||||
final SqmQuerySpec<?> querySpec = sqm.getQuerySpec();
|
||||
SqmSelectStatement<KeyedResult<R>> statement, NodeBuilder builder) {
|
||||
final SqmQuerySpec<?> querySpec = statement.getQuerySpec();
|
||||
final List<? extends JpaSelection<?>> items = querySpec.getSelectClause().getSelectionItems();
|
||||
if ( items.size() == 1 ) {
|
||||
final JpaSelection<?> selected = items.get(0);
|
||||
if ( selected instanceof SqmRoot) {
|
||||
statement.orderBy( keyDefinition.stream().map( order -> sortSpecification( statement, order ) )
|
||||
.collect( toList() ) );
|
||||
final SqmFrom<?,?> root = (SqmFrom<?,?>) selected;
|
||||
sqm.select( keySelection( keyDefinition, root, selected, builder ) );
|
||||
statement.select( keySelection( keyDefinition, root, selected, builder ) );
|
||||
if ( keyValues != null ) {
|
||||
final SqmPredicate restriction = keyRestriction( keyDefinition, keyValues, root, builder );
|
||||
final SqmPredicate queryWhere = querySpec.getRestriction();
|
||||
sqm.where( queryWhere == null ? restriction : builder.and( queryWhere, restriction ) );
|
||||
statement.where( queryWhere == null ? restriction : builder.and( queryWhere, restriction ) );
|
||||
}
|
||||
return sqm;
|
||||
return statement;
|
||||
}
|
||||
else {
|
||||
throw new IllegalQueryOperationException("Select item was not an entity type");
|
||||
|
|
|
@ -564,7 +564,7 @@ public class SqmUtil {
|
|||
}
|
||||
}
|
||||
|
||||
static <T> JpaOrder sortSpecification(SqmSelectStatement<T> sqm, Order<? super T> order) {
|
||||
static JpaOrder sortSpecification(SqmSelectStatement<?> sqm, Order<?> order) {
|
||||
final List<SqmSelectableNode<?>> items = sqm.getQuerySpec().getSelectClause().getSelectionItems();
|
||||
int element = order.getElement();
|
||||
if ( element < 1) {
|
||||
|
|
|
@ -14,6 +14,8 @@ import java.time.LocalDate;
|
|||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SessionFactory
|
||||
@DomainModel(annotatedClasses = KeyBasedPagingTest.Person.class)
|
||||
|
@ -35,17 +37,38 @@ public class KeyBasedPagingTest {
|
|||
session.createSelectionQuery("from Person", Person.class)
|
||||
.getKeyedResultList(Page.first(5)
|
||||
.keyedBy(Order.asc(Person.class, "ssn")));
|
||||
assertTrue( first.isFirstPage() );
|
||||
assertFalse( first.isLastPage() );
|
||||
assertEquals(5,first.getResultList().size());
|
||||
int page = 1;
|
||||
int page = 0;
|
||||
KeyedResultList<Person> next = first;
|
||||
while ( next.getNextPage() != null ) {
|
||||
while ( !next.isLastPage() ) {
|
||||
page ++;
|
||||
next = session.createSelectionQuery("from Person", Person.class)
|
||||
.getKeyedResultList(next.getNextPage());
|
||||
assertEquals(page, next.getPage().getPage().getNumber());
|
||||
page ++;
|
||||
if ( !next.isLastPage() ) {
|
||||
assertEquals(5, next.getResultList().size());
|
||||
}
|
||||
assertEquals(4, page);
|
||||
}
|
||||
assertEquals(3, page);
|
||||
assertEquals(2, next.getResultList().size());
|
||||
assertTrue( next.isLastPage() );
|
||||
assertFalse( next.isFirstPage() );
|
||||
|
||||
KeyedResultList<Person> previous = next;
|
||||
while ( !previous.isFirstPage() ) {
|
||||
page --;
|
||||
previous = session.createSelectionQuery("from Person where dob > :minDate", Person.class)
|
||||
.setParameter("minDate", LocalDate.of(1970, 2, 5))
|
||||
.getKeyedResultList(previous.getPreviousPage());
|
||||
assertEquals(page, previous.getPage().getPage().getNumber());
|
||||
assertEquals(5, previous.getResultList().size());
|
||||
}
|
||||
assertEquals(0, page);
|
||||
assertEquals(5, previous.getResultList().size());
|
||||
assertTrue( previous.isFirstPage() );
|
||||
assertFalse( previous.isLastPage() );
|
||||
});
|
||||
|
||||
scope.inSession(session -> {
|
||||
|
@ -74,18 +97,39 @@ public class KeyBasedPagingTest {
|
|||
.setParameter("minDate", LocalDate.of(1970, 2, 5))
|
||||
.getKeyedResultList(Page.first(5)
|
||||
.keyedBy(Order.asc(Person.class, "ssn")));
|
||||
assertTrue( first.isFirstPage() );
|
||||
assertFalse( first.isLastPage() );
|
||||
assertEquals(5,first.getResultList().size());
|
||||
int page = 1;
|
||||
int page = 0;
|
||||
KeyedResultList<Person> next = first;
|
||||
while ( next.getNextPage() != null ) {
|
||||
while ( !next.isLastPage() ) {
|
||||
page ++;
|
||||
next = session.createSelectionQuery("from Person where dob > :minDate", Person.class)
|
||||
.setParameter("minDate", LocalDate.of(1970, 2, 5))
|
||||
.getKeyedResultList(next.getNextPage());
|
||||
assertEquals(page, next.getPage().getPage().getNumber());
|
||||
page ++;
|
||||
if ( !next.isLastPage() ) {
|
||||
assertEquals(5, next.getResultList().size());
|
||||
}
|
||||
assertEquals(3, page);
|
||||
}
|
||||
assertEquals(2, page);
|
||||
assertEquals(2, next.getResultList().size());
|
||||
assertTrue( next.isLastPage() );
|
||||
assertFalse( next.isFirstPage() );
|
||||
|
||||
KeyedResultList<Person> previous = next;
|
||||
while ( !previous.isFirstPage() ) {
|
||||
page --;
|
||||
previous = session.createSelectionQuery("from Person where dob > :minDate", Person.class)
|
||||
.setParameter("minDate", LocalDate.of(1970, 2, 5))
|
||||
.getKeyedResultList(previous.getPreviousPage());
|
||||
assertEquals(page, previous.getPage().getPage().getNumber());
|
||||
assertEquals(5, previous.getResultList().size());
|
||||
}
|
||||
assertEquals(0, page);
|
||||
assertEquals(5, previous.getResultList().size());
|
||||
assertTrue( previous.isFirstPage() );
|
||||
assertFalse( previous.isLastPage() );
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue