HHH-17779 support for key-based pagination

lots of fixes / improvements
This commit is contained in:
Gavin King 2024-02-27 10:30:06 +01:00
parent 483279c748
commit 920377ccfc
8 changed files with 234 additions and 108 deletions

View File

@ -78,6 +78,6 @@ public class KeyedPage<R> {
@Internal
public KeyedPage<R> nextPage(List<Comparable<?>> keyOfLastResult) {
return new KeyedPage<>( keyDefinition, page, keyOfLastResult );
return new KeyedPage<>( keyDefinition, page.next(), keyOfLastResult );
}
}

View File

@ -36,15 +36,45 @@ public class KeyedResultList<R> {
this.nextPage = nextPage;
}
/**
* The results on the current page.
*/
public List<R> getResultList() {
return resultList;
}
/**
* The {@linkplain Page#getSize() size} and
* approximate {@linkplain Page#getNumber()
* page number} of the current page.
*/
public KeyedPage<R> getPage() {
return page;
}
/**
* The specification of the next page of results,
* if there are more results, or {@code null} if
* it is known that there are no more results
* after this page.
*/
public KeyedPage<R> getNextPage() {
return nextPage;
}
/**
* @return {@code true} if this is known to be the
* last page of results.
*/
public boolean isLastPage() {
return nextPage == null;
}
/**
* @return {@code true} if this is the first page
* of results.
*/
public boolean isFirstPage() {
return page.getPage().isFirst();
}
}

View File

@ -90,6 +90,9 @@ public class Page {
return new KeyedPage<>( List.of(keyDefinition), this );
}
public <R> KeyedPage<R> keyedBy(List<Order<? super R>> keyDefinition) {
if ( keyDefinition.isEmpty() ) {
throw new IllegalArgumentException("Key definition must not be empty");
}
return new KeyedPage<>( keyDefinition, this );
}
}

View File

@ -32,6 +32,7 @@ import org.hibernate.query.criteria.JpaSearchedCase;
import org.hibernate.query.criteria.JpaSelection;
import org.hibernate.query.criteria.JpaSimpleCase;
import org.hibernate.query.criteria.JpaWindow;
import org.hibernate.query.criteria.ValueHandlingMode;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
import org.hibernate.query.sqm.tree.domain.SqmBagJoin;
@ -85,6 +86,8 @@ public interface NodeBuilder extends HibernateCriteriaBuilder {
QueryEngine getQueryEngine();
void setCriteriaValueHandlingMode(ValueHandlingMode criteriaValueHandlingMode);
<R> SqmTuple<R> tuple(
Class<R> tupleType,
SqmExpression<?>... expressions);

View File

@ -6,12 +6,9 @@
*/
package org.hibernate.query.sqm.internal;
import jakarta.persistence.criteria.Expression;
import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.query.IllegalQueryOperationException;
import org.hibernate.query.IllegalSelectQueryException;
import org.hibernate.query.KeyedPage;
import org.hibernate.query.KeyedResultList;
@ -19,7 +16,7 @@ import org.hibernate.query.Order;
import org.hibernate.query.Page;
import org.hibernate.query.QueryLogging;
import org.hibernate.query.SelectionQuery;
import org.hibernate.query.criteria.JpaSelection;
import org.hibernate.query.criteria.ValueHandlingMode;
import org.hibernate.query.hql.internal.QuerySplitter;
import org.hibernate.query.spi.AbstractSelectionQuery;
import org.hibernate.query.spi.MutableQueryOptions;
@ -27,21 +24,15 @@ import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.SelectQueryPlan;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.tree.SqmStatement;
import org.hibernate.query.sqm.tree.domain.SqmPath;
import org.hibernate.query.sqm.tree.from.SqmFrom;
import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.predicate.SqmPredicate;
import org.hibernate.query.sqm.tree.predicate.SqmWhereClause;
import org.hibernate.query.sqm.tree.select.SqmQuerySpec;
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.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.hibernate.cfg.QuerySettings.FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH;
import static org.hibernate.query.sqm.internal.KeyBasedPagination.paginate;
import static org.hibernate.query.sqm.internal.SqmUtil.sortSpecification;
import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext;
@ -143,98 +134,18 @@ abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
return this;
}
static class KeyedResult<R> {
final R result;
final List<Comparable<?>> key;
public KeyedResult(R result, List<Comparable<?>> key) {
this.result = result;
this.key = key;
}
public R getResult() {
return result;
}
public List<Comparable<?>> getKey() {
return key;
}
}
private SqmSelectStatement<KeyedResult<R>> keyedQuery(
private SqmSelectStatement<KeyedResult<R>> paginateQuery(
List<Order<? super R>> keyDefinition, List<Comparable<?>> keyValues) {
@SuppressWarnings("unchecked")
final SqmSelectStatement<KeyedResult<R>> sqm =
(SqmSelectStatement<KeyedResult<R>>)
getSqmSelectStatement().copy( noParamCopyContext() );
final NodeBuilder builder = sqm.nodeBuilder();
final SqmQuerySpec<?> querySpec = sqm.getQuerySpec();
final List<? extends JpaSelection<?>> items = querySpec.getSelectClause().getSelectionItems();
if ( items.size() == 1 ) {
final JpaSelection<?> selected = items.get(0);
if ( selected instanceof SqmRoot ) {
final List<SqmPath<?>> newItems = new ArrayList<>();
final SqmFrom<?,?> root = (SqmFrom<?,?>) selected;
SqmPredicate restriction = querySpec.getRestriction();
if ( restriction==null && keyValues != null ) {
restriction = builder.disjunction();
}
for ( int i = 0; i < keyDefinition.size(); i++ ) {
// ordering by an attribute of the returned entity
final Order<? super R> key = keyDefinition.get(i);
if ( keyValues != null ) {
@SuppressWarnings("rawtypes")
final Comparable keyValue = keyValues.get(i);
restriction = builder.or( restriction,
keyPredicate( root, key, keyValue, newItems, keyValues, builder ) );
}
newItems.add( root.get( key.getAttributeName() ) );
}
sqm.select( builder.construct((Class) KeyedResult.class,
asList(selected, builder.construct(List.class, newItems)) ) );
if ( keyValues != null ) {
sqm.where( restriction );
}
return sqm;
}
else {
throw new IllegalQueryOperationException("Select item was not an entity type");
}
}
else {
throw new IllegalQueryOperationException("Query has multiple items in the select list");
}
//TODO: find a better way handle parameters
builder.setCriteriaValueHandlingMode(ValueHandlingMode.INLINE);
return paginate( keyDefinition, keyValues, sqm, builder );
}
private <C extends Comparable<? super C>> SqmPredicate keyPredicate(
SqmFrom<?, ?> selected, Order<? super R> key, C keyValue,
List<SqmPath<?>> previousKeys, List<Comparable<?>> keyValues,
NodeBuilder builder) {
if ( !key.getEntityClass().isAssignableFrom( selected.getJavaType() ) ) {
throw new IllegalQueryOperationException("Select item was of wrong entity type");
}
final Expression<? extends C> path = selected.get( key.getAttributeName() );
// TODO: use a parameter here and create a binding for it
// @SuppressWarnings("unchecked")
// final Class<C> valueClass = (Class<C>) keyValue.getClass();
// final JpaParameterExpression<C> parameter = builder.parameter(valueClass);
// setParameter( parameter, keyValue );
SqmPredicate predicate;
switch ( key.getDirection() ) {
case ASCENDING:
predicate = builder.greaterThan( path, keyValue );
break;
case DESCENDING:
predicate = builder.lessThan( path, keyValue );
break;
default:
throw new AssertionFailure("Unrecognized key direction");
}
for ( int i = 0; i < previousKeys.size(); i++ ) {
final SqmPath keyPath = previousKeys.get(i);
predicate = builder.and( predicate, keyPath.equalTo( keyValues.get(i) ) );
}
return predicate;
}
@Override
public KeyedResultList<R> getKeyedResultList(KeyedPage<R> keyedPage) {
@ -246,27 +157,34 @@ abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
final List<Comparable<?>> key = keyedPage.getKey();
setOrder( keyDefinition );
setMaxResults( page.getMaxResults() );
setMaxResults( page.getMaxResults() + 1 );
if ( key == null ) {
setFirstResult( page.getFirstResult() );
}
// getQueryOptions().setQueryPlanCachingEnabled( false );
final List<KeyedResult<R>> executed =
buildConcreteQueryPlan( keyedQuery( keyDefinition, key ), getQueryOptions() )
buildConcreteQueryPlan( paginateQuery( keyDefinition, key ), getQueryOptions() )
.performList(this);
final KeyedPage<R> nextPage = keyedPage.nextPage( getKeyOfLastResult( executed, key ) );
return new KeyedResultList<>( collectResultList( executed ), keyedPage, nextPage );
return new KeyedResultList<>( collectResults( executed, page.getSize() ),
keyedPage, getNextPage( keyedPage, executed ) );
}
private static <R> List<R> collectResultList(List<KeyedResult<R>> executed) {
return executed.stream()
.map(KeyedResult::getResult)
.collect(toList());
private static <R> List<R> collectResults(List<KeyedResult<R>> executed, int pageSize) {
final int size = executed.size();
final List<R> resultList = new ArrayList<>( size );
for (int i = 0; i < size && i < pageSize; i++) {
resultList.add( executed.get(i).getResult() );
}
return resultList;
}
private static <R> List<Comparable<?>> getKeyOfLastResult(
List<KeyedResult<R>> executed, List<Comparable<?>> key) {
return executed.isEmpty() ? key : executed.get(executed.size() - 1).getKey();
private static <R> KeyedPage<R> getNextPage(KeyedPage<R> keyedPage, List<KeyedResult<R>> executed) {
final int pageSize = keyedPage.getPage().getSize();
return executed.size() == pageSize + 1
? keyedPage.nextPage( executed.get(pageSize - 1).getKey() )
: null;
}
public abstract Class<R> getExpectedResultType();

View File

@ -0,0 +1,135 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.query.sqm.internal;
import jakarta.persistence.criteria.Expression;
import org.hibernate.AssertionFailure;
import org.hibernate.query.IllegalQueryOperationException;
import org.hibernate.query.Order;
import org.hibernate.query.SortDirection;
import org.hibernate.query.criteria.JpaCompoundSelection;
import org.hibernate.query.criteria.JpaSelection;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.tree.domain.SqmPath;
import org.hibernate.query.sqm.tree.from.SqmFrom;
import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.predicate.SqmPredicate;
import org.hibernate.query.sqm.tree.select.SqmQuerySpec;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import java.util.ArrayList;
import java.util.List;
import static java.util.Arrays.asList;
/**
* Manipulation of SQM query tree for key-based pagination.
*
* @author Gavin King
*/
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();
final List<? extends JpaSelection<?>> items = querySpec.getSelectClause().getSelectionItems();
if ( items.size() == 1 ) {
final JpaSelection<?> selected = items.get(0);
if ( selected instanceof SqmRoot) {
final SqmFrom<?,?> root = (SqmFrom<?,?>) selected;
sqm.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 ) );
}
return sqm;
}
else {
throw new IllegalQueryOperationException("Select item was not an entity type");
}
}
else {
throw new IllegalQueryOperationException("Query has multiple items in the select list");
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static <R> SqmPredicate keyRestriction(
List<Order<? super R>> keyDefinition,
List<Comparable<?>> keyValues,
SqmFrom<?, ?> root,
NodeBuilder builder) {
final List<SqmPath<?>> keyPaths = new ArrayList<>();
for ( Order<? super R> key : keyDefinition ) {
keyPaths.add( root.get( key.getAttributeName() ) );
}
SqmPredicate restriction = null;
for (int i = 0; i < keyDefinition.size(); i++ ) {
// ordering by an attribute of the returned entity
final SortDirection direction = keyDefinition.get(i).getDirection();
final SqmPath key = keyPaths.get(i);
final Comparable keyValue = keyValues.get(i);
final List<SqmPath<?>> previousKeys = keyPaths.subList(0, i);
final SqmPredicate predicate = keyPredicate( key, keyValue, direction, previousKeys, keyValues, builder );
restriction = restriction == null ? predicate : builder.or( restriction, predicate );
}
return restriction;
}
private static <R> JpaCompoundSelection<KeyedResult<R>> keySelection(
List<Order<? super R>> keyDefinition,
SqmFrom<?, ?> root, JpaSelection<?> selected,
NodeBuilder builder) {
final List<SqmPath<?>> items = new ArrayList<>();
for ( Order<? super R> key : keyDefinition ) {
if ( !key.getEntityClass().isAssignableFrom( selected.getJavaType() ) ) {
throw new IllegalQueryOperationException("Select item was of wrong entity type");
}
// ordering by an attribute of the returned entity
items.add( root.get( key.getAttributeName() ) );
}
return keyedResultConstructor( selected, builder, items );
}
private static <R> JpaCompoundSelection<KeyedResult<R>> keyedResultConstructor(
JpaSelection<?> selected, NodeBuilder builder, List<SqmPath<?>> newItems) {
@SuppressWarnings({"rawtypes", "unchecked"})
final Class<KeyedResult<R>> resultClass = (Class) KeyedResult.class;
return builder.construct( resultClass, asList( selected, builder.construct(List.class, newItems ) ) );
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static <C extends Comparable<? super C>> SqmPredicate keyPredicate(
Expression<? extends C> key, C keyValue, SortDirection direction,
List<SqmPath<?>> previousKeys, List<Comparable<?>> keyValues,
NodeBuilder builder) {
// TODO: use a parameter here and create a binding for it
// @SuppressWarnings("unchecked")
// final Class<C> valueClass = (Class<C>) keyValue.getClass();
// final JpaParameterExpression<C> parameter = builder.parameter(valueClass);
// setParameter( parameter, keyValue );
SqmPredicate predicate;
switch ( direction ) {
case ASCENDING:
predicate = builder.greaterThan( key, keyValue );
break;
case DESCENDING:
predicate = builder.lessThan( key, keyValue );
break;
default:
throw new AssertionFailure("Unrecognized key direction");
}
for ( int i = 0; i < previousKeys.size(); i++ ) {
final SqmPath keyPath = previousKeys.get(i);
predicate = builder.and( predicate, keyPath.equalTo( keyValues.get(i) ) );
}
return predicate;
}
}

View File

@ -0,0 +1,33 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.query.sqm.internal;
import java.util.List;
/**
* Intermediate holder class for results of queries
* executed using key-based pagination.
*
* @author Gavin King
*/
class KeyedResult<R> {
final R result;
final List<Comparable<?>> key;
public KeyedResult(R result, List<Comparable<?>> key) {
this.result = result;
this.key = key;
}
public R getResult() {
return result;
}
public List<Comparable<?>> getKey() {
return key;
}
}

View File

@ -197,7 +197,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext,
private final transient boolean jpaComplianceEnabled;
private final transient QueryEngine queryEngine;
private final transient Supplier<SessionFactoryImplementor> sessionFactory;
private final transient ValueHandlingMode criteriaValueHandlingMode;
private transient ValueHandlingMode criteriaValueHandlingMode;
private transient BasicType<Boolean> booleanType;
private transient BasicType<Integer> integerType;
private transient BasicType<Long> longType;
@ -224,6 +224,10 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext,
}
}
public void setCriteriaValueHandlingMode(ValueHandlingMode criteriaValueHandlingMode) {
this.criteriaValueHandlingMode = criteriaValueHandlingMode;
}
@Override
public JpaMetamodel getDomainModel() {
return getSessionFactory().getJpaMetamodel();