HHH-17779 support for key-based pagination

This commit is contained in:
Gavin King 2024-02-26 13:58:36 +01:00
parent 2f4a6ebcaf
commit 4fbcfbdaba
12 changed files with 240 additions and 68 deletions

View File

@ -13,7 +13,6 @@ import org.hibernate.Incubating;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.graph.spi.RootGraphImplementor;
import org.hibernate.internal.util.NullnessUtil;
import org.jboss.logging.Logger;

View File

@ -24,7 +24,6 @@ import java.util.Set;
import org.hibernate.CacheMode;
import org.hibernate.ConnectionAcquisitionMode;
import org.hibernate.FetchNotFoundException;
import org.hibernate.Filter;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;

View File

@ -29,7 +29,6 @@ import org.hibernate.loader.ast.spi.Loadable;
import org.hibernate.loader.ast.spi.Loader;
import org.hibernate.metamodel.CollectionClassification;
import org.hibernate.metamodel.mapping.AttributeMapping;
import org.hibernate.metamodel.mapping.BasicValuedModelPart;
import org.hibernate.metamodel.mapping.CollectionPart;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.EntityValuedModelPart;

View File

@ -0,0 +1,51 @@
/*
* 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;
import java.util.List;
import static java.util.Collections.unmodifiableList;
/**
* Support for pagination based on a unique key of the result
* set instead of the {@link Page#getFirstResult() offset}.
*
* @since 6.5
*
* @author Gavin King
*/
public class KeyedPage<R> {
private final List<Order<? super R>> keyDefinition;
private final Page page;
private final List<Comparable<?>> key;
KeyedPage(List<Order<? super R>> keyDefinition, Page page) {
this( keyDefinition, page, null );
}
public KeyedPage(List<Order<? super R>> keyDefinition, Page page, List<Comparable<?>> key) {
this.page = page;
this.keyDefinition = unmodifiableList(keyDefinition);
this.key = key;
}
public List<Order<? super R>> getKeyDefinition() {
return keyDefinition;
}
public Page getPage() {
return page;
}
/**
* Null key indicates that the {@linkplain Page#getNumber() page number}
* should be used. This is useful to obtain an initial page of results.
*/
public List<Comparable<?>> getKey() {
return key;
}
}

View File

@ -8,6 +8,8 @@ package org.hibernate.query;
import org.hibernate.Incubating;
import java.util.List;
/**
* Identifies a page of query results by {@linkplain #size page size}
* and {@linkplain #number page number}.
@ -50,7 +52,7 @@ public class Page {
return size*number;
}
private Page(int size, int number) {
Page(int size, int number) {
if ( size <= 0 ) {
throw new IllegalArgumentException("page size must be strictly positive");
}
@ -83,4 +85,8 @@ public class Page {
public Page first() {
return first( size );
}
public <R> KeyedPage<R> keyedBy(List<Order<? super R>> keyDefinition) {
return new KeyedPage<>( keyDefinition, this );
}
}

View File

@ -903,6 +903,11 @@ public interface Query<R> extends SelectionQuery<R>, MutationQuery, TypedQuery<R
return this;
}
@Override
default SelectionQuery<R> setPage(KeyedPage<R> page) {
throw new UnsupportedOperationException("keyed paging not supported");
}
@Override
Query<R> setHint(String hintName, Object value);

View File

@ -395,6 +395,16 @@ public interface SelectionQuery<R> extends CommonQueryContract {
@Incubating
SelectionQuery<R> setPage(Page page);
/**
* Set the {@linkplain KeyedPage keyed page} of results to return.
*
* @see KeyedPage
*
* @since 6.5
*/
@Incubating
SelectionQuery<R> setPage(KeyedPage<R> page);
/**
* Obtain the {@link CacheMode} in effect for this query. By default,
* the query inherits the {@link CacheMode} of the session from which

View File

@ -6,22 +6,40 @@
*/
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.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.hql.internal.QuerySplitter;
import org.hibernate.query.spi.AbstractSelectionQuery;
import org.hibernate.query.spi.MutableQueryOptions;
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.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.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.sqm.internal.SqmUtil.sortSpecification;
import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext;
/**
* @author Gavin King
@ -73,8 +91,115 @@ abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
}
public abstract SqmStatement<R> getSqmStatement();
protected abstract void setSqmStatement(SqmSelectStatement<R> statement);
public abstract DomainParameterXref getDomainParameterXref();
public abstract TupleMetadata getTupleMetadata();
private SqmSelectStatement<R> getSqmSelectStatement() {
final SqmStatement<R> sqmStatement = getSqmStatement();
if ( sqmStatement instanceof SqmSelectStatement ) {
return (SqmSelectStatement<R>) sqmStatement;
}
else {
throw new IllegalSelectQueryException( "Not a select query" );
}
}
@Override
public SelectionQuery<R> setOrder(List<Order<? super R>> orderList) {
SqmSelectStatement<R> sqm = getSqmSelectStatement();
sqm = sqm.copy( noParamCopyContext() );
final SqmSelectStatement<R> select = sqm;
sqm.orderBy( orderList.stream().map( order -> sortSpecification( select, order ) )
.collect( toList() ) );
// TODO: when the QueryInterpretationCache can handle caching criteria queries,
// simply cache the new SQM as if it were a criteria query, and remove this:
getQueryOptions().setQueryPlanCachingEnabled( false );
setSqmStatement( sqm );
return this;
}
@Override
public SelectionQuery<R> setOrder(Order<? super R> order) {
SqmSelectStatement<R> sqm = getSqmSelectStatement();
sqm = sqm.copy( noParamCopyContext() );
sqm.orderBy( sortSpecification( sqm, order ) );
// TODO: when the QueryInterpretationCache can handle caching criteria queries,
// simply cache the new SQM as if it were a criteria query, and remove this:
getQueryOptions().setQueryPlanCachingEnabled( false );
setSqmStatement( sqm );
return this;
}
@Override
public SelectionQuery<R> setPage(Page page) {
setMaxResults( page.getMaxResults() );
setFirstResult( page.getFirstResult() );
return this;
}
@Override
public SelectionQuery<R> setPage(KeyedPage<R> page) {
setOrder( page.getKeyDefinition() );
setMaxResults( page.getPage().getMaxResults() );
if ( page.getKey() == null ) {
setFirstResult( page.getPage().getFirstResult() );
}
else {
addRestrictions( page.getKeyDefinition(), page.getKey() );
}
return this;
}
private void addRestrictions(List<Order<? super R>> keyDefinition, List<Comparable<?>> keyValues) {
SqmSelectStatement<R> sqm = getSqmSelectStatement();
sqm = sqm.copy( noParamCopyContext() );
final SqmQuerySpec<R> querySpec = sqm.getQuerySpec();
final SqmWhereClause whereClause = querySpec.getWhereClause();
final List<? extends JpaSelection<?>> items = querySpec.getSelectClause().getSelectionItems();
if ( items.size() == 1 ) {
final JpaSelection<?> selected = items.get(0);
for ( int i = 0; i < keyDefinition.size(); i++ ) {
// ordering by an attribute of the returned entity
if ( selected instanceof SqmRoot ) {
whereClause.applyPredicate( keyPredicate(
(SqmFrom<?,?>) selected,
keyDefinition.get(i),
(Comparable) keyValues.get(i))
);
}
else {
throw new IllegalQueryOperationException("Select item was not an entity type");
}
}
}
else {
throw new IllegalQueryOperationException("Query has multiple items in the select list");
}
// TODO: when the QueryInterpretationCache can handle caching criteria queries,
// simply cache the new SQM as if it were a criteria query, and remove this:
getQueryOptions().setQueryPlanCachingEnabled( false );
setSqmStatement( sqm );
}
private <C extends Comparable<? super C>> SqmPredicate keyPredicate(
SqmFrom<?, ?> selected, Order<? super R> key, C keyValue) {
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() );
final NodeBuilder builder = getSqmStatement().nodeBuilder();
switch ( key.getDirection() ) {
case ASCENDING:
return builder.greaterThan( path, keyValue);
case DESCENDING:
return builder.lessThan( path, keyValue);
default:
throw new AssertionFailure("Unrecognized key direction");
}
}
public abstract Class<R> getExpectedResultType();
protected SelectQueryPlan<R> buildSelectQueryPlan() {

View File

@ -47,9 +47,10 @@ import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.query.BindableType;
import org.hibernate.query.IllegalQueryOperationException;
import org.hibernate.query.IllegalSelectQueryException;
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.KeyedPage;
import org.hibernate.query.Order;
import org.hibernate.query.Page;
import org.hibernate.query.Query;
import org.hibernate.query.QueryParameter;
import org.hibernate.query.ResultListTransformer;
@ -109,7 +110,6 @@ import jakarta.persistence.PersistenceException;
import jakarta.persistence.TemporalType;
import org.hibernate.sql.results.spi.SingleResultConsumer;
import static java.util.stream.Collectors.toList;
import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE;
import static org.hibernate.jpa.HibernateHints.HINT_CACHE_MODE;
import static org.hibernate.jpa.HibernateHints.HINT_CACHE_REGION;
@ -126,10 +126,8 @@ import static org.hibernate.query.sqm.internal.AppliedGraphs.containsCollectionF
import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.createInterpretationsKey;
import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.generateNonSelectKey;
import static org.hibernate.query.sqm.internal.SqmUtil.isSelect;
import static org.hibernate.query.sqm.internal.SqmUtil.sortSpecification;
import static org.hibernate.query.sqm.internal.SqmUtil.verifyIsNonSelectStatement;
import static org.hibernate.query.sqm.internal.TypecheckUtil.assertAssignable;
import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext;
/**
* {@link Query} implementation based on an SQM
@ -426,6 +424,11 @@ public class QuerySqmImpl<R>
return sqm;
}
@Override
protected void setSqmStatement(SqmSelectStatement<R> sqm) {
this.sqm = sqm;
}
@Override
public DomainParameterXref getDomainParameterXref() {
return domainParameterXref;
@ -903,36 +906,27 @@ public class QuerySqmImpl<R>
}
@Override
public Query<R> setOrder(List<Order<? super R>> orderList) {
if ( sqm instanceof SqmSelectStatement ) {
sqm = sqm.copy( noParamCopyContext() );
final SqmSelectStatement<R> select = (SqmSelectStatement<R>) sqm;
select.orderBy( orderList.stream().map( order -> sortSpecification( select, order ) )
.collect( toList() ) );
// TODO: when the QueryInterpretationCache can handle caching criteria queries,
// simply cache the new SQM as if it were a criteria query, and remove this:
getQueryOptions().setQueryPlanCachingEnabled( false );
public Query<R> setOrder(Order<? super R> order) {
super.setOrder(order);
return this;
}
else {
throw new IllegalSelectQueryException( "Not a select query" );
}
}
@Override
public Query<R> setOrder(Order<? super R> order) {
if ( sqm instanceof SqmSelectStatement ) {
sqm = sqm.copy( noParamCopyContext() );
SqmSelectStatement<R> select = (SqmSelectStatement<R>) sqm;
select.orderBy( sortSpecification( select, order ) );
// TODO: when the QueryInterpretationCache can handle caching criteria queries,
// simply cache the new SQM as if it were a criteria query, and remove this:
getQueryOptions().setQueryPlanCachingEnabled( false );
public Query<R> setOrder(List<Order<? super R>> orders) {
super.setOrder(orders);
return this;
}
else {
throw new IllegalSelectQueryException( "Not a select query" );
@Override
public Query<R> setPage(Page page) {
super.setPage(page);
return this;
}
@Override
public Query<R> setPage(KeyedPage<R> page) {
super.setPage(page);
return this;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -31,7 +31,6 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.internal.util.collections.IdentitySet;
import org.hibernate.query.BindableType;
import org.hibernate.query.Order;
import org.hibernate.query.Page;
import org.hibernate.query.QueryParameter;
import org.hibernate.query.SelectionQuery;
@ -63,7 +62,6 @@ import org.hibernate.sql.results.internal.TupleMetadata;
import org.hibernate.sql.results.spi.ResultsConsumer;
import org.hibernate.sql.results.spi.SingleResultConsumer;
import static java.util.stream.Collectors.toList;
import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE;
import static org.hibernate.jpa.HibernateHints.HINT_CACHE_MODE;
import static org.hibernate.jpa.HibernateHints.HINT_CACHE_REGION;
@ -77,7 +75,6 @@ import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_STORE_MODE;
import static org.hibernate.query.spi.SqlOmittingQueryOptions.omitSqlQueryOptions;
import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.createInterpretationsKey;
import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType;
import static org.hibernate.query.sqm.internal.SqmUtil.sortSpecification;
/**
* @author Steve Ebersole
@ -245,6 +242,11 @@ public class SqmSelectionQueryImpl<R> extends AbstractSqmSelectionQuery<R>
return sqm;
}
@Override
protected void setSqmStatement(SqmSelectStatement<R> sqm) {
this.sqm = sqm;
}
@Override
public DomainParameterXref getDomainParameterXref() {
return domainParameterXref;
@ -275,34 +277,6 @@ public class SqmSelectionQueryImpl<R> extends AbstractSqmSelectionQuery<R>
// return this;
// }
@Override
public SelectionQuery<R> setPage(Page page) {
setMaxResults( page.getMaxResults() );
setFirstResult( page.getFirstResult() );
return this;
}
@Override
public SelectionQuery<R> setOrder(List<Order<? super R>> orderList) {
sqm = sqm.copy( SqmCopyContext.noParamCopyContext() );
sqm.orderBy( orderList.stream().map( order -> sortSpecification( sqm, order ) )
.collect( toList() ) );
// TODO: when the QueryInterpretationCache can handle caching criteria queries,
// simply cache the new SQM as if it were a criteria query, and remove this:
getQueryOptions().setQueryPlanCachingEnabled( false );
return this;
}
@Override
public SelectionQuery<R> setOrder(Order<? super R> order) {
sqm = sqm.copy( SqmCopyContext.noParamCopyContext() );
sqm.orderBy( sortSpecification( sqm, order ) );
// TODO: when the QueryInterpretationCache can handle caching criteria queries,
// simply cache the new SQM as if it were a criteria query, and remove this:
getQueryOptions().setQueryPlanCachingEnabled( false );
return this;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// execution

View File

@ -568,10 +568,12 @@ public class SqmUtil {
final List<SqmSelectableNode<?>> items = sqm.getQuerySpec().getSelectClause().getSelectionItems();
int element = order.getElement();
if ( element < 1) {
throw new IllegalQueryOperationException("Cannot order by element " + element + " (the first select item is element 1)");
throw new IllegalQueryOperationException("Cannot order by element " + element
+ " (the first select item is element 1)");
}
if ( element > items.size() ) {
throw new IllegalQueryOperationException("Cannot order by element " + element + " (there are only " + items.size() + " select items)");
throw new IllegalQueryOperationException("Cannot order by element " + element
+ " (there are only " + items.size() + " select items)");
}
final SqmSelectableNode<?> selected = items.get( element-1 );

View File

@ -25,10 +25,12 @@ import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.query.BindableType;
import org.hibernate.query.KeyedPage;
import org.hibernate.query.Order;
import org.hibernate.query.Page;
import org.hibernate.query.ParameterMetadata;
import org.hibernate.query.QueryParameter;
import org.hibernate.query.SelectionQuery;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.sqm.tree.SqmStatement;
import org.hibernate.sql.results.spi.ResultsConsumer;
@ -196,6 +198,12 @@ public abstract class DelegatingSqmSelectionQueryImplementor<R> implements SqmSe
return this;
}
@Override
public SelectionQuery<R> setPage(KeyedPage<R> page) {
getDelegate().setPage( page );
return this;
}
@Override
public CacheMode getCacheMode() {
return getDelegate().getCacheMode();