From 920377ccfcc21166d119850fe88ff09d4aef6a5b Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 27 Feb 2024 10:30:06 +0100 Subject: [PATCH] HHH-17779 support for key-based pagination lots of fixes / improvements --- .../java/org/hibernate/query/KeyedPage.java | 2 +- .../org/hibernate/query/KeyedResultList.java | 30 ++++ .../main/java/org/hibernate/query/Page.java | 3 + .../org/hibernate/query/sqm/NodeBuilder.java | 3 + .../internal/AbstractSqmSelectionQuery.java | 130 ++++------------- .../sqm/internal/KeyBasedPagination.java | 135 ++++++++++++++++++ .../query/sqm/internal/KeyedResult.java | 33 +++++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 6 +- 8 files changed, 234 insertions(+), 108 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyedResult.java diff --git a/hibernate-core/src/main/java/org/hibernate/query/KeyedPage.java b/hibernate-core/src/main/java/org/hibernate/query/KeyedPage.java index 345951c79a..2022963d79 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/KeyedPage.java +++ b/hibernate-core/src/main/java/org/hibernate/query/KeyedPage.java @@ -78,6 +78,6 @@ public class KeyedPage { @Internal public KeyedPage nextPage(List> keyOfLastResult) { - return new KeyedPage<>( keyDefinition, page, keyOfLastResult ); + return new KeyedPage<>( keyDefinition, page.next(), keyOfLastResult ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/KeyedResultList.java b/hibernate-core/src/main/java/org/hibernate/query/KeyedResultList.java index d2b74314cc..1725bdc830 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/KeyedResultList.java +++ b/hibernate-core/src/main/java/org/hibernate/query/KeyedResultList.java @@ -36,15 +36,45 @@ public class KeyedResultList { this.nextPage = nextPage; } + /** + * The results on the current page. + */ public List getResultList() { return resultList; } + /** + * The {@linkplain Page#getSize() size} and + * approximate {@linkplain Page#getNumber() + * page number} of the current page. + */ public KeyedPage 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 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(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/Page.java b/hibernate-core/src/main/java/org/hibernate/query/Page.java index 6d85167967..9ca1d07ebe 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/Page.java +++ b/hibernate-core/src/main/java/org/hibernate/query/Page.java @@ -90,6 +90,9 @@ public class Page { return new KeyedPage<>( List.of(keyDefinition), this ); } public KeyedPage keyedBy(List> keyDefinition) { + if ( keyDefinition.isEmpty() ) { + throw new IllegalArgumentException("Key definition must not be empty"); + } return new KeyedPage<>( keyDefinition, this ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index e78c4f519e..9aef289a86 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -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); + SqmTuple tuple( Class tupleType, SqmExpression... expressions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java index dbdea8ddec..cbee6f63e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java @@ -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 extends AbstractSelectionQuery { return this; } - static class KeyedResult { - final R result; - final List> key; - public KeyedResult(R result, List> key) { - this.result = result; - this.key = key; - } - - public R getResult() { - return result; - } - - public List> getKey() { - return key; - } - } - - private SqmSelectStatement> keyedQuery( + private SqmSelectStatement> paginateQuery( List> keyDefinition, List> keyValues) { @SuppressWarnings("unchecked") final SqmSelectStatement> sqm = (SqmSelectStatement>) getSqmSelectStatement().copy( noParamCopyContext() ); final NodeBuilder builder = sqm.nodeBuilder(); - final SqmQuerySpec querySpec = sqm.getQuerySpec(); - final List> items = querySpec.getSelectClause().getSelectionItems(); - if ( items.size() == 1 ) { - final JpaSelection selected = items.get(0); - if ( selected instanceof SqmRoot ) { - final List> 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 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 > SqmPredicate keyPredicate( - SqmFrom selected, Order key, C keyValue, - List> previousKeys, List> keyValues, - NodeBuilder builder) { - if ( !key.getEntityClass().isAssignableFrom( selected.getJavaType() ) ) { - throw new IllegalQueryOperationException("Select item was of wrong entity type"); - } - final Expression path = selected.get( key.getAttributeName() ); - // TODO: use a parameter here and create a binding for it -// @SuppressWarnings("unchecked") -// final Class valueClass = (Class) keyValue.getClass(); -// final JpaParameterExpression 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 getKeyedResultList(KeyedPage keyedPage) { @@ -246,27 +157,34 @@ abstract class AbstractSqmSelectionQuery extends AbstractSelectionQuery { final List> key = keyedPage.getKey(); setOrder( keyDefinition ); - setMaxResults( page.getMaxResults() ); + setMaxResults( page.getMaxResults() + 1 ); if ( key == null ) { setFirstResult( page.getFirstResult() ); } +// getQueryOptions().setQueryPlanCachingEnabled( false ); final List> executed = - buildConcreteQueryPlan( keyedQuery( keyDefinition, key ), getQueryOptions() ) + buildConcreteQueryPlan( paginateQuery( keyDefinition, key ), getQueryOptions() ) .performList(this); - final KeyedPage 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 List collectResultList(List> executed) { - return executed.stream() - .map(KeyedResult::getResult) - .collect(toList()); + private static List collectResults(List> executed, int pageSize) { + final int size = executed.size(); + final List resultList = new ArrayList<>( size ); + for (int i = 0; i < size && i < pageSize; i++) { + resultList.add( executed.get(i).getResult() ); + } + return resultList; } - private static List> getKeyOfLastResult( - List> executed, List> key) { - return executed.isEmpty() ? key : executed.get(executed.size() - 1).getKey(); + private static KeyedPage getNextPage(KeyedPage keyedPage, List> executed) { + final int pageSize = keyedPage.getPage().getSize(); + return executed.size() == pageSize + 1 + ? keyedPage.nextPage( executed.get(pageSize - 1).getKey() ) + : null; } public abstract Class getExpectedResultType(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java new file mode 100644 index 0000000000..8d7145ace1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java @@ -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 SqmSelectStatement> paginate( + List> keyDefinition, List> keyValues, + SqmSelectStatement> sqm, NodeBuilder builder) { + final SqmQuerySpec querySpec = sqm.getQuerySpec(); + final List> 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 SqmPredicate keyRestriction( + List> keyDefinition, + List> keyValues, + SqmFrom root, + NodeBuilder builder) { + final List> keyPaths = new ArrayList<>(); + for ( Order 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> 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 JpaCompoundSelection> keySelection( + List> keyDefinition, + SqmFrom root, JpaSelection selected, + NodeBuilder builder) { + final List> items = new ArrayList<>(); + for ( Order 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 JpaCompoundSelection> keyedResultConstructor( + JpaSelection selected, NodeBuilder builder, List> newItems) { + @SuppressWarnings({"rawtypes", "unchecked"}) + final Class> resultClass = (Class) KeyedResult.class; + return builder.construct( resultClass, asList( selected, builder.construct(List.class, newItems ) ) ); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static > SqmPredicate keyPredicate( + Expression key, C keyValue, SortDirection direction, + List> previousKeys, List> keyValues, + NodeBuilder builder) { + // TODO: use a parameter here and create a binding for it +// @SuppressWarnings("unchecked") +// final Class valueClass = (Class) keyValue.getClass(); +// final JpaParameterExpression 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; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyedResult.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyedResult.java new file mode 100644 index 0000000000..f5807bac62 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyedResult.java @@ -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 { + final R result; + final List> key; + + public KeyedResult(R result, List> key) { + this.result = result; + this.key = key; + } + + public R getResult() { + return result; + } + + public List> getKey() { + return key; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 9665f6b8d0..4afde125f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -197,7 +197,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, private final transient boolean jpaComplianceEnabled; private final transient QueryEngine queryEngine; private final transient Supplier sessionFactory; - private final transient ValueHandlingMode criteriaValueHandlingMode; + private transient ValueHandlingMode criteriaValueHandlingMode; private transient BasicType booleanType; private transient BasicType integerType; private transient BasicType 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();