HHH-17779 support for key-based pagination

This commit is contained in:
Gavin King 2024-02-26 15:56:04 +01:00
parent 5af80551ad
commit b9e01fec4f
5 changed files with 129 additions and 22 deletions

View File

@ -45,6 +45,7 @@ import org.hibernate.procedure.spi.ParameterStrategy;
import org.hibernate.procedure.spi.ProcedureCallImplementor;
import org.hibernate.procedure.spi.ProcedureParameterImplementor;
import org.hibernate.query.BindableType;
import org.hibernate.query.KeyedResultList;
import org.hibernate.query.Order;
import org.hibernate.query.OutputableType;
import org.hibernate.query.Query;
@ -969,6 +970,11 @@ public class ProcedureCallImpl<R>
throw new UnsupportedOperationException( "getResultCount() not implemented for ProcedureCall/StoredProcedureQuery" );
}
@Override
public KeyedResultList<R> getKeyedResultList() {
throw new UnsupportedOperationException("getKeyedResultList() not implemented for ProcedureCall/StoredProcedureQuery");
}
@Override
public ScrollableResultsImplementor<R> scroll(ScrollMode scrollMode) {
throw new UnsupportedOperationException( "scroll() is not implemented for ProcedureCall/StoredProcedureQuery" );

View File

@ -0,0 +1,41 @@
/*
* 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;
/**
* 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 KeyedResultList<R> {
private final List<R> resultList;
private final KeyedPage<R> page;
private final KeyedPage<R> nextPage;
public KeyedResultList(List<R> resultList, KeyedPage<R> page, KeyedPage<R> nextPage) {
this.resultList = resultList;
this.page = page;
this.nextPage = nextPage;
}
public List<R> getResultList() {
return resultList;
}
public KeyedPage<R> getPage() {
return page;
}
public KeyedPage<R> getNextPage() {
return nextPage;
}
}

View File

@ -220,6 +220,9 @@ public interface SelectionQuery<R> extends CommonQueryContract {
@Incubating
long getResultCount();
@Incubating
KeyedResultList<R> getKeyedResultList();
SelectionQuery<R> setHint(String hintName, Object value);
/**

View File

@ -42,6 +42,7 @@ import org.hibernate.jpa.spi.NativeQueryTupleTransformer;
import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.query.BindableType;
import org.hibernate.query.KeyedResultList;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.Order;
import org.hibernate.query.ParameterMetadata;
@ -637,6 +638,11 @@ public class NativeQueryImpl<R>
return createCountQueryPlan().executeQuery( context, new SingleResultConsumer<>() );
}
@Override
public KeyedResultList<R> getKeyedResultList() {
throw new UnsupportedOperationException("native queries do not support key-based pagination");
}
protected SelectQueryPlan<R> resolveSelectQueryPlan() {
if ( isCacheableQuery() ) {
final QueryInterpretationCache.Key cacheKey = generateSelectInterpretationsKey( resultSetMapping );

View File

@ -14,6 +14,7 @@ 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;
import org.hibernate.query.Order;
import org.hibernate.query.Page;
import org.hibernate.query.QueryLogging;
@ -26,6 +27,7 @@ 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;
@ -33,9 +35,13 @@ 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 org.hibernate.sql.results.spi.ListResultsConsumer;
import org.hibernate.sql.results.spi.ListResultsConsumer.UniqueSemantic;
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.SqmUtil.sortSpecification;
@ -46,6 +52,8 @@ import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext;
*/
abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
private KeyedPage<R> keyedPage;
AbstractSqmSelectionQuery(SharedSessionContractImplementor session) {
super(session);
}
@ -141,48 +149,62 @@ abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
@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() );
}
keyedPage = page;
return this;
}
private void addRestrictions(List<Order<? super R>> keyDefinition, List<Comparable<?>> keyValues) {
SqmSelectStatement<R> sqm = getSqmSelectStatement();
sqm = sqm.copy( noParamCopyContext() );
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>> keyed(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<R> querySpec = sqm.getQuerySpec();
final SqmQuerySpec<?> querySpec = sqm.getQuerySpec();
final SqmWhereClause whereClause = querySpec.getWhereClause();
final List<? extends JpaSelection<?>> items = querySpec.getSelectClause().getSelectionItems();
final List<SqmPath<?>> newItems = new ArrayList<>();
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),
builder)
);
final Order<? super R> key = keyDefinition.get(i);
final SqmFrom<?,?> root = (SqmFrom<?,?>) selected;
if ( keyValues != null ) {
@SuppressWarnings("rawtypes")
final Comparable keyValue = keyValues.get(i);
whereClause.applyPredicate( keyPredicate( root, key, keyValue, builder ) );
}
newItems.add( root.get( key.getAttributeName() ) );
}
else {
throw new IllegalQueryOperationException("Select item was not an entity type");
}
}
sqm.select( builder.construct((Class) KeyedResult.class,
asList(selected, builder.construct(List.class, newItems)) ) );
return sqm;
}
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(
@ -206,6 +228,35 @@ abstract class AbstractSqmSelectionQuery<R> extends AbstractSelectionQuery<R> {
}
}
@Override
public KeyedResultList<R> getKeyedResultList() {
if ( keyedPage == null ) {
throw new IllegalStateException( "KeyedPage was not set" );
}
final Page page = keyedPage.getPage();
final List<Order<? super R>> keyDefinition = keyedPage.getKeyDefinition();
final List<Comparable<?>> key = keyedPage.getKey();
setOrder( keyDefinition );
setMaxResults( page.getMaxResults() );
if ( key == null ) {
setFirstResult( page.getFirstResult() );
}
else {
keyed( keyDefinition, key );
}
final SqmSelectStatement<KeyedResult<R>> sqm = keyed( keyDefinition, key );
final ConcreteSqmSelectQueryPlan<KeyedResult<R>> plan =
buildConcreteQueryPlan( sqm, null, null, getQueryOptions() );
final List<KeyedResult<R>> executed =
plan.executeQuery( this, new ListResultsConsumer<>(UniqueSemantic.NONE) );
final List<Comparable<?>> keyOfLastResult =
executed.isEmpty() ? key : executed.get( executed.size()-1 ).getKey();
return new KeyedResultList<>( executed.stream().map(KeyedResult::getResult).collect(toList()),
keyedPage, new KeyedPage<>(keyDefinition, page.next(), keyOfLastResult) );
}
public abstract Class<R> getExpectedResultType();
protected SelectQueryPlan<R> buildSelectQueryPlan() {