From 1180be0a0fca0e826a0fd775c1fe8e9c99180194 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 19 Feb 2024 12:53:55 +0100 Subject: [PATCH] HHH-16931 add SelectionQuery.getResultCount() --- .../procedure/internal/ProcedureCallImpl.java | 11 +- .../org/hibernate/query/SelectionQuery.java | 13 ++ .../query/hql/internal/QuerySplitter.java | 25 +-- .../query/sql/internal/NativeQueryImpl.java | 54 +++++- .../query/sqm/internal/QuerySqmImpl.java | 60 ++++--- .../sqm/internal/SqmSelectionQueryImpl.java | 34 +++- ...elegatingSqmSelectionQueryImplementor.java | 5 + .../query/sqm/tree/SqmStatement.java | 1 - .../tree/domain/AbstractSqmAttributeJoin.java | 8 +- .../sqm/tree/domain/AbstractSqmFrom.java | 98 ++++++----- .../query/sqm/tree/from/SqmAttributeJoin.java | 5 +- .../tree/select/AbstractSqmSelectQuery.java | 12 +- .../sqm/tree/select/SqmSelectStatement.java | 157 ++++++++++++------ .../query/sqm/tree/select/SqmSubQuery.java | 16 +- .../sql/results/spi/SingleResultConsumer.java | 47 ++++++ .../orm/test/query/count/CountTest.java | 152 +++++++++++++++++ .../test/query/criteria/CountQueryTests.java | 15 +- 17 files changed, 545 insertions(+), 168 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/count/CountTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java index 2978559ab4..2a3a82987a 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java @@ -943,7 +943,7 @@ public class ProcedureCallImpl try { final Output rtn = outputs().getCurrent(); if ( !(rtn instanceof ResultSetOutput) ) { - throw new IllegalStateException( "Current CallableStatement ou was not a ResultSet, but getResultList was called" ); + throw new IllegalStateException( "Current CallableStatement was not a ResultSet, but getResultList was called" ); } //noinspection unchecked @@ -964,14 +964,19 @@ public class ProcedureCallImpl } } + @Override + public long getResultCount() { + throw new UnsupportedOperationException( "getResultCount() not implemented for ProcedureCall/StoredProcedureQuery" ); + } + @Override public ScrollableResultsImplementor scroll(ScrollMode scrollMode) { - throw new UnsupportedOperationException( "Query#scroll is not valid for ProcedureCall/StoredProcedureQuery" ); + throw new UnsupportedOperationException( "scroll() is not implemented for ProcedureCall/StoredProcedureQuery" ); } @Override protected ScrollableResultsImplementor doScroll(ScrollMode scrollMode) { - throw new UnsupportedOperationException( "Query#scroll is not valid for ProcedureCall/StoredProcedureQuery" ); + throw new UnsupportedOperationException( "scroll() is not implemented for ProcedureCall/StoredProcedureQuery" ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java index ed458b5173..5f9790b49c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/SelectionQuery.java @@ -207,6 +207,19 @@ public interface SelectionQuery extends CommonQueryContract { */ Optional uniqueResultOptional(); + /** + * Determine the size of the query result list that would be + * returned by calling {@link #getResultList()} with no + * {@linkplain #getFirstResult() offset} or + * {@linkplain #getMaxResults() limit} applied to the query. + * + * @return the size of the list that would be returned + * + * @since 6.5 + */ + @Incubating + long getResultCount(); + SelectionQuery setHint(String hintName, Object value); /** diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java index 84918e6161..1032063a64 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java @@ -8,15 +8,12 @@ package org.hibernate.query.hql.internal; import java.util.Set; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.model.domain.EntityDomainType; -import org.hibernate.query.sqm.internal.SimpleSqmCopyContext; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; -import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.select.SqmQueryGroup; import org.hibernate.query.sqm.tree.select.SqmQueryPart; @@ -31,20 +28,21 @@ import org.hibernate.query.sqm.tree.select.SqmSelectStatement; */ public class QuerySplitter { - public static SqmSelectStatement[] split( - SqmSelectStatement statement, - SessionFactoryImplementor sessionFactory) { + public static SqmSelectStatement[] split(SqmSelectStatement statement) { // We only allow unmapped polymorphism in a very restricted way. Specifically, // the unmapped polymorphic reference can only be a root and can be the only // root. Use that restriction to locate the unmapped polymorphic reference final SqmRoot unmappedPolymorphicReference = findUnmappedPolymorphicReference( statement.getQueryPart() ); if ( unmappedPolymorphicReference == null ) { - return new SqmSelectStatement[] { statement }; + @SuppressWarnings("unchecked") + SqmSelectStatement[] sqmSelectStatement = new SqmSelectStatement[] { statement }; + return sqmSelectStatement; } final SqmPolymorphicRootDescriptor unmappedPolymorphicDescriptor = (SqmPolymorphicRootDescriptor) unmappedPolymorphicReference.getReferencedPathSource(); final Set> implementors = unmappedPolymorphicDescriptor.getImplementors(); + @SuppressWarnings("unchecked") final SqmSelectStatement[] expanded = new SqmSelectStatement[ implementors.size() ]; int i = 0; @@ -97,20 +95,21 @@ public class QuerySplitter { return (S) statement.copy( context ); } - public static SqmDeleteStatement[] split( - SqmDeleteStatement statement, - SessionFactoryImplementor sessionFactory) { + public static SqmDeleteStatement[] split(SqmDeleteStatement statement) { // We only allow unmapped polymorphism in a very restricted way. Specifically, // the unmapped polymorphic reference can only be a root and can be the only // root. Use that restriction to locate the unmapped polymorphic reference final SqmRoot unmappedPolymorphicReference = findUnmappedPolymorphicReference( statement ); if ( unmappedPolymorphicReference == null ) { - return new SqmDeleteStatement[] { statement }; + @SuppressWarnings("unchecked") + SqmDeleteStatement[] sqmDeleteStatement = new SqmDeleteStatement[] { statement }; + return sqmDeleteStatement; } final SqmPolymorphicRootDescriptor unmappedPolymorphicDescriptor = (SqmPolymorphicRootDescriptor) unmappedPolymorphicReference.getReferencedPathSource(); final Set> implementors = unmappedPolymorphicDescriptor.getImplementors(); + @SuppressWarnings("unchecked") final SqmDeleteStatement[] expanded = new SqmDeleteStatement[ implementors.size() ]; int i = 0; @@ -125,6 +124,8 @@ public class QuerySplitter { if ( queryPart.getTarget().getReferencedPathSource() instanceof SqmPolymorphicRootDescriptor ) { return queryPart.getTarget(); } - return null; + else { + return null; + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index e285320942..d3038461dd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -50,6 +50,7 @@ import org.hibernate.query.Query; import org.hibernate.query.QueryParameter; import org.hibernate.query.ResultListTransformer; import org.hibernate.query.TupleTransformer; +import org.hibernate.query.internal.DelegatingDomainQueryExecutionContext; import org.hibernate.query.internal.ParameterMetadataImpl; import org.hibernate.query.internal.QueryOptionsImpl; import org.hibernate.query.internal.QueryParameterBindingsImpl; @@ -58,7 +59,9 @@ import org.hibernate.query.named.NamedResultSetMappingMemento; import org.hibernate.query.results.Builders; import org.hibernate.query.results.ResultBuilder; import org.hibernate.query.results.ResultSetMapping; +import org.hibernate.query.results.ResultSetMappingImpl; import org.hibernate.query.results.dynamic.DynamicFetchBuilderLegacy; +import org.hibernate.query.results.dynamic.DynamicResultBuilderBasicStandard; import org.hibernate.query.results.dynamic.DynamicResultBuilderEntityCalculated; import org.hibernate.query.results.dynamic.DynamicResultBuilderEntityStandard; import org.hibernate.query.results.dynamic.DynamicResultBuilderInstantiation; @@ -71,6 +74,7 @@ import org.hibernate.query.spi.NonSelectQueryPlan; import org.hibernate.query.spi.ParameterMetadataImplementor; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.spi.QueryInterpretationCache; +import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.QueryParameterImplementor; @@ -87,6 +91,7 @@ import org.hibernate.query.sql.spi.SelectInterpretationsKey; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; +import org.hibernate.sql.results.spi.SingleResultConsumer; import org.hibernate.transform.ResultTransformer; import org.hibernate.type.BasicType; import org.hibernate.type.BasicTypeReference; @@ -621,6 +626,17 @@ public class NativeQueryImpl return resolveSelectQueryPlan().performList( this ); } + @Override + public long getResultCount() { + final DelegatingDomainQueryExecutionContext context = new DelegatingDomainQueryExecutionContext(this) { + @Override + public QueryOptions getQueryOptions() { + return QueryOptions.NONE; + } + }; + return createCountQueryPlan().executeQuery( context, new SingleResultConsumer<>() ); + } + protected SelectQueryPlan resolveSelectQueryPlan() { if ( isCacheableQuery() ) { final QueryInterpretationCache.Key cacheKey = generateSelectInterpretationsKey( resultSetMapping ); @@ -647,7 +663,7 @@ public class NativeQueryImpl @Override public List getQueryParameterOccurrences() { - return NativeQueryImpl.this.parameterOccurrences; + return parameterOccurrences; } @Override @@ -665,6 +681,42 @@ public class NativeQueryImpl .createQueryPlan( queryDefinition, getSessionFactory() ); } + private NativeSelectQueryPlan createCountQueryPlan() { + final BasicType longType = getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType(Long.class); + final String sqlString = expandParameterLists(); + final NativeSelectQueryDefinition queryDefinition = new NativeSelectQueryDefinition<>() { + @Override + public String getSqlString() { + return "select count(*) from (" + sqlString + ") a_"; + } + + @Override + public boolean isCallable() { + return false; + } + + @Override + public List getQueryParameterOccurrences() { + return parameterOccurrences; + } + + @Override + public ResultSetMapping getResultSetMapping() { + final ResultSetMappingImpl mapping = new ResultSetMappingImpl( "", true ); + mapping.addResultBuilder( new DynamicResultBuilderBasicStandard( 1, longType ) ); + return mapping; + } + + @Override + public Set getAffectedTableNames() { + return querySpaces; + } + }; + + return getSessionFactory().getQueryEngine().getNativeQueryInterpreter() + .createQueryPlan( queryDefinition, getSessionFactory() ); + } + protected String expandParameterLists() { if ( parameterOccurrences == null || parameterOccurrences.isEmpty() ) { return sqlString; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 8258257666..436524062a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -111,6 +111,7 @@ import jakarta.persistence.LockModeType; import jakarta.persistence.Parameter; 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; @@ -131,6 +132,7 @@ 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 @@ -490,6 +492,20 @@ public class QuerySqmImpl } } + @Override + public long getResultCount() { + verifySelect(); + final DelegatingDomainQueryExecutionContext context = new DelegatingDomainQueryExecutionContext(this) { + @Override + public QueryOptions getQueryOptions() { + return QueryOptions.NONE; + } + }; + final SqmSelectStatement sqmStatement = (SqmSelectStatement) getSqmStatement(); + return buildConcreteSelectQueryPlan( sqmStatement.createCountQuery(), Long.class, null, getQueryOptions() ) + .executeQuery( context, new SingleResultConsumer<>() ); + } + protected List doList() { verifySelect(); @@ -613,39 +629,40 @@ public class QuerySqmImpl } private SelectQueryPlan buildSelectQueryPlan() { - final SqmSelectStatement[] concreteSqmStatements = QuerySplitter.split( - (SqmSelectStatement) getSqmStatement(), - getSession().getFactory() - ); + final SqmSelectStatement[] concreteSqmStatements = + QuerySplitter.split( (SqmSelectStatement) getSqmStatement() ); if ( concreteSqmStatements.length > 1 ) { return buildAggregatedSelectQueryPlan( concreteSqmStatements ); } else { - return buildConcreteSelectQueryPlan( concreteSqmStatements[0], getResultType(), getQueryOptions() ); + return buildConcreteSelectQueryPlan( concreteSqmStatements[0] ); } } - private SelectQueryPlan buildAggregatedSelectQueryPlan(SqmSelectStatement[] concreteSqmStatements) { - //noinspection unchecked + private SelectQueryPlan buildAggregatedSelectQueryPlan(SqmSelectStatement[] concreteSqmStatements) { + @SuppressWarnings("unchecked") final SelectQueryPlan[] aggregatedQueryPlans = new SelectQueryPlan[ concreteSqmStatements.length ]; - // todo (6.0) : we want to make sure that certain thing (ResultListTransformer, etc) only get applied at the aggregator-level - for ( int i = 0, x = concreteSqmStatements.length; i < x; i++ ) { - aggregatedQueryPlans[i] = buildConcreteSelectQueryPlan( - concreteSqmStatements[i], - getResultType(), - getQueryOptions() - ); + aggregatedQueryPlans[i] = buildConcreteSelectQueryPlan( concreteSqmStatements[i] ); } - return new AggregatedSelectQueryPlanImpl<>( aggregatedQueryPlans ); } + private SelectQueryPlan buildConcreteSelectQueryPlan(SqmSelectStatement concreteSqmStatement) { + return buildConcreteSelectQueryPlan( + concreteSqmStatement, + getResultType(), + tupleMetadata, + getQueryOptions() + ); + } + private SelectQueryPlan buildConcreteSelectQueryPlan( - SqmSelectStatement concreteSqmStatement, + SqmSelectStatement concreteSqmStatement, Class resultType, + TupleMetadata tupleMetadata, QueryOptions queryOptions) { return new ConcreteSqmSelectQueryPlan<>( concreteSqmStatement, @@ -742,11 +759,8 @@ public class QuerySqmImpl } private NonSelectQueryPlan buildDeleteQueryPlan() { - final SqmDeleteStatement[] concreteSqmStatements = QuerySplitter.split( - (SqmDeleteStatement) getSqmStatement(), - getSessionFactory() - ); - + final SqmDeleteStatement[] concreteSqmStatements = + QuerySplitter.split( (SqmDeleteStatement) getSqmStatement() ); return concreteSqmStatements.length > 1 ? buildAggregatedDeleteQueryPlan( concreteSqmStatements ) : buildConcreteDeleteQueryPlan( concreteSqmStatements[0] ); @@ -948,7 +962,7 @@ public class QuerySqmImpl @Override public Query setOrder(List> orderList) { if ( sqm instanceof SqmSelectStatement ) { - sqm = sqm.copy( SqmCopyContext.noParamCopyContext() ); + sqm = sqm.copy( noParamCopyContext() ); final SqmSelectStatement select = (SqmSelectStatement) sqm; select.orderBy( orderList.stream().map( order -> sortSpecification( select, order ) ) .collect( toList() ) ); @@ -965,7 +979,7 @@ public class QuerySqmImpl @Override public Query setOrder(Order order) { if ( sqm instanceof SqmSelectStatement ) { - sqm = sqm.copy( SqmCopyContext.noParamCopyContext() ); + sqm = sqm.copy( noParamCopyContext() ); SqmSelectStatement select = (SqmSelectStatement) sqm; select.orderBy( sortSpecification( select, order ) ); // TODO: when the QueryInterpretationCache can handle caching criteria queries, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index c0d0c35513..bfc0023409 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -66,6 +66,7 @@ import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.results.internal.TupleMetadata; import org.hibernate.sql.results.spi.ResultsConsumer; +import org.hibernate.sql.results.spi.SingleResultConsumer; import org.hibernate.type.descriptor.java.JavaType; import static java.util.stream.Collectors.toList; @@ -131,7 +132,7 @@ public class SqmSelectionQueryImpl extends AbstractSelectionQuery else { final SqmSelection selection = selections.get(0); if ( selection!=null ) { - JavaType javaType = selection.getNodeJavaType(); + final JavaType javaType = selection.getNodeJavaType(); if ( javaType != null) { return javaType.getJavaTypeClass(); } @@ -317,6 +318,18 @@ public class SqmSelectionQueryImpl extends AbstractSelectionQuery resetCallback(); } + @Override + public long getResultCount() { + final DelegatingDomainQueryExecutionContext context = new DelegatingDomainQueryExecutionContext(this) { + @Override + public QueryOptions getQueryOptions() { + return QueryOptions.NONE; + } + }; + return buildConcreteQueryPlan( getSqmStatement().createCountQuery(), Long.class, null, getQueryOptions() ) + .executeQuery( context, new SingleResultConsumer<>() ); + } + protected List doList() { final SqmSelectStatement sqmStatement = getSqmStatement(); final boolean containsCollectionFetches = sqmStatement.containsCollectionFetches(); @@ -413,17 +426,13 @@ public class SqmSelectionQueryImpl extends AbstractSelectionQuery } private SelectQueryPlan buildQueryPlan() { - final SqmSelectStatement[] concreteSqmStatements = QuerySplitter.split( - (SqmSelectStatement) getSqmStatement(), - getSession().getFactory() - ); - + final SqmSelectStatement[] concreteSqmStatements = QuerySplitter.split( getSqmStatement() ); return concreteSqmStatements.length > 1 ? buildAggregatedQueryPlan( concreteSqmStatements ) : buildConcreteQueryPlan( concreteSqmStatements[0], getQueryOptions() ); } - private SelectQueryPlan buildAggregatedQueryPlan(SqmSelectStatement[] concreteSqmStatements) { + private SelectQueryPlan buildAggregatedQueryPlan(SqmSelectStatement[] concreteSqmStatements) { //noinspection unchecked final SelectQueryPlan[] aggregatedQueryPlans = new SelectQueryPlan[ concreteSqmStatements.length ]; // todo (6.0) : we want to make sure that certain thing (ResultListTransformer, etc) only get applied at the aggregator-level @@ -434,7 +443,15 @@ public class SqmSelectionQueryImpl extends AbstractSelectionQuery } private SelectQueryPlan buildConcreteQueryPlan( - SqmSelectStatement concreteSqmStatement, + SqmSelectStatement concreteSqmStatement, + QueryOptions queryOptions) { + return buildConcreteQueryPlan( concreteSqmStatement, expectedResultType, tupleMetadata, queryOptions); + } + + private ConcreteSqmSelectQueryPlan buildConcreteQueryPlan( + SqmSelectStatement concreteSqmStatement, + Class expectedResultType, + TupleMetadata tupleMetadata, QueryOptions queryOptions) { return new ConcreteSqmSelectQueryPlan<>( concreteSqmStatement, @@ -447,7 +464,6 @@ public class SqmSelectionQueryImpl extends AbstractSelectionQuery } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // InterpretationsKeySource diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/DelegatingSqmSelectionQueryImplementor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/DelegatingSqmSelectionQueryImplementor.java index 7e5e1789ce..1b6291807c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/DelegatingSqmSelectionQueryImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/DelegatingSqmSelectionQueryImplementor.java @@ -94,6 +94,11 @@ public abstract class DelegatingSqmSelectionQueryImplementor implements SqmSe return getDelegate().getResultList(); } + @Override + public long getResultCount() { + return getDelegate().getResultCount(); + } + @Override public ScrollableResults scroll() { return getDelegate().scroll(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmStatement.java index 2682db3c60..420ad9c70f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmStatement.java @@ -9,7 +9,6 @@ package org.hibernate.query.sqm.tree; import java.util.Collections; import java.util.Map; import java.util.Set; -import java.util.function.Supplier; import org.hibernate.query.criteria.JpaQueryableCriteria; import org.hibernate.query.sqm.SqmQuerySource; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java index 51125281c7..813045a7bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java @@ -33,7 +33,7 @@ public abstract class AbstractSqmAttributeJoin extends AbstractSqmQualifiedJoin implements SqmAttributeJoin { - private final boolean fetched; + private boolean fetched; public AbstractSqmAttributeJoin( SqmFrom lhs, @@ -84,10 +84,16 @@ public abstract class AbstractSqmAttributeJoin return getJavaTypeDescriptor(); } + @Override public boolean isFetched() { return fetched; } + @Override + public void clearFetched() { + fetched = false; + } + @Override public X accept(SemanticQueryWalker walker) { return walker.visitQualifiedAttributeJoin( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index f1bd1cae1f..1b1281636d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -15,7 +15,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import org.hibernate.metamodel.mapping.ModelPartContainer; +import org.hibernate.Internal; import org.hibernate.metamodel.model.domain.BagPersistentAttribute; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ListPersistentAttribute; @@ -198,42 +198,42 @@ public abstract class AbstractSqmFrom extends AbstractSqmPath implements return sqmPath; } - private ModelPartContainer findModelPartContainer(SqmAttributeJoin attributeJoin, SqmCreationState creationState) { - final SqmFrom lhs = attributeJoin.getLhs(); - if ( lhs instanceof SqmAttributeJoin ) { - final SqmAttributeJoin lhsAttributeJoin = (SqmAttributeJoin) lhs; - if ( lhsAttributeJoin.getReferencedPathSource() instanceof EntityDomainType ) { - final String entityName = ( (EntityDomainType) lhsAttributeJoin.getReferencedPathSource() ).getHibernateEntityName(); - return (ModelPartContainer) creationState.getCreationContext() - .getJpaMetamodel() - .getMappingMetamodel() - .getEntityDescriptor( entityName ) - .findSubPart( attributeJoin.getAttribute().getName(), null ); - } - else { - return (ModelPartContainer) findModelPartContainer( lhsAttributeJoin, creationState ) - .findSubPart( attributeJoin.getAttribute().getName(), null ); - } - } - else { - final String entityName; - if ( lhs instanceof SqmRoot ) { - entityName = ( (SqmRoot) lhs ).getEntityName(); - } - else if ( lhs instanceof SqmEntityJoin ) { - entityName = ( (SqmEntityJoin) lhs ).getEntityName(); - } - else { - assert lhs instanceof SqmCrossJoin; - entityName = ( (SqmCrossJoin) lhs ).getEntityName(); - } - return (ModelPartContainer) creationState.getCreationContext() - .getJpaMetamodel() - .getMappingMetamodel() - .getEntityDescriptor( entityName ) - .findSubPart( attributeJoin.getAttribute().getName(), null ); - } - } +// private ModelPartContainer findModelPartContainer(SqmAttributeJoin attributeJoin, SqmCreationState creationState) { +// final SqmFrom lhs = attributeJoin.getLhs(); +// if ( lhs instanceof SqmAttributeJoin ) { +// final SqmAttributeJoin lhsAttributeJoin = (SqmAttributeJoin) lhs; +// if ( lhsAttributeJoin.getReferencedPathSource() instanceof EntityDomainType ) { +// final String entityName = ( (EntityDomainType) lhsAttributeJoin.getReferencedPathSource() ).getHibernateEntityName(); +// return (ModelPartContainer) creationState.getCreationContext() +// .getJpaMetamodel() +// .getMappingMetamodel() +// .getEntityDescriptor( entityName ) +// .findSubPart( attributeJoin.getAttribute().getName(), null ); +// } +// else { +// return (ModelPartContainer) findModelPartContainer( lhsAttributeJoin, creationState ) +// .findSubPart( attributeJoin.getAttribute().getName(), null ); +// } +// } +// else { +// final String entityName; +// if ( lhs instanceof SqmRoot ) { +// entityName = ( (SqmRoot) lhs ).getEntityName(); +// } +// else if ( lhs instanceof SqmEntityJoin ) { +// entityName = ( (SqmEntityJoin) lhs ).getEntityName(); +// } +// else { +// assert lhs instanceof SqmCrossJoin; +// entityName = ( (SqmCrossJoin) lhs ).getEntityName(); +// } +// return (ModelPartContainer) creationState.getCreationContext() +// .getJpaMetamodel() +// .getMappingMetamodel() +// .getEntityDescriptor( entityName ) +// .findSubPart( attributeJoin.getAttribute().getName(), null ); +// } +// } @Override public boolean hasJoins() { @@ -254,6 +254,29 @@ public abstract class AbstractSqmFrom extends AbstractSqmPath implements findRoot().addOrderedJoin( join ); } + @Internal + public void removeLeftFetchJoins() { + if ( joins != null ) { + for ( SqmJoin join : new ArrayList<>(joins) ) { + if ( join instanceof SqmAttributeJoin ) { + final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) join; + if ( attributeJoin.isFetched() ) { + if ( join.getSqmJoinType() == SqmJoinType.LEFT ) { + joins.remove( join ); + final List> orderedJoins = findRoot().getOrderedJoins(); + if (orderedJoins != null) { + orderedJoins.remove( join ); + } + } + else { + attributeJoin.clearFetched(); + } + } + } + } + } + } + @Override public void visitSqmJoins(Consumer> consumer) { if ( joins != null ) { @@ -386,7 +409,6 @@ public abstract class AbstractSqmFrom extends AbstractSqmPath implements } @Override - @SuppressWarnings("unchecked") public SqmMapJoin join(MapAttribute attribute, JoinType jt) { final SqmMapJoin join = buildMapJoin( (MapPersistentAttribute) attribute, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java index 9dc35b6744..f72b1fef42 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmAttributeJoin.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.sqm.tree.from; +import org.hibernate.Internal; import org.hibernate.Remove; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.criteria.JpaFetch; @@ -37,6 +38,9 @@ public interface SqmAttributeJoin extends SqmQualifiedJoin, JpaFetch extends SqmQualifiedJoin, JpaFetch } protected Map> copyCteStatements(SqmCopyContext context) { - final Map> cteStatements = new LinkedHashMap<>( this.cteStatements.size() ); - for ( Map.Entry> entry : this.cteStatements.entrySet() ) { - cteStatements.put( entry.getKey(), entry.getValue().copy( context ) ); + final Map> copies = new LinkedHashMap<>( cteStatements.size() ); + for ( Map.Entry> entry : cteStatements.entrySet() ) { + copies.put( entry.getKey(), entry.getValue().copy( context ) ); } - return cteStatements; + return copies; } @Override @@ -154,7 +156,7 @@ public abstract class AbstractSqmSelectQuery if ( name == null || name.isBlank() ) { throw new IllegalArgumentException( "Illegal empty CTE name" ); } - if ( !Character.isAlphabetic( name.charAt( 0 ) ) ) { + if ( !isAlphabetic( name.charAt( 0 ) ) ) { throw new IllegalArgumentException( String.format( "Illegal CTE name [%s]. Names must start with an alphabetic character!", diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java index d6cd358792..86944c8f77 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java @@ -21,6 +21,7 @@ import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Selection; +import org.hibernate.Internal; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.criteria.JpaCriteriaQuery; import org.hibernate.query.criteria.JpaExpression; @@ -28,16 +29,17 @@ import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.SqmQuerySource; -import org.hibernate.query.sqm.internal.NoParamSqmCopyContext; import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; -import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.from.SqmFromClause; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import static org.hibernate.query.sqm.SqmQuerySource.CRITERIA; +import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; import static org.hibernate.query.sqm.tree.jpa.ParameterCollector.collectParameters; /** @@ -87,7 +89,7 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements */ public SqmSelectStatement(Class resultJavaType, NodeBuilder nodeBuilder) { super( resultJavaType, nodeBuilder ); - this.querySource = SqmQuerySource.CRITERIA; + this.querySource = CRITERIA; getQuerySpec().setSelectClause( new SqmSelectClause( false, nodeBuilder ) ); getQuerySpec().setFromClause( new SqmFromClause() ); } @@ -99,7 +101,7 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements */ public SqmSelectStatement(SqmSelectStatement original) { super( original.getQueryPart(), original.getCteStatementMap(), original.getResultType(), original.nodeBuilder() ); - this.querySource = SqmQuerySource.CRITERIA; + this.querySource = CRITERIA; } private SqmSelectStatement( @@ -113,6 +115,54 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements this.parameters = parameters; } + /** + * A query that returns the number of results of this query. + * + * @since 6.5 + */ + @Internal + public SqmSelectStatement countQuery() { + final SqmSelectStatement copy = copy( noParamCopyContext() ); + final SqmQuerySpec querySpec = copy.getQuerySpec(); + final SqmQueryPart queryPart = copy.getQueryPart(); + //TODO: detect queries with no 'group by', but aggregate functions + // in 'select' list (we don't even need to hit the database to + // know they return exactly one row) + if ( queryPart.isSimpleQueryPart() + && !querySpec.isDistinct() + && querySpec.getGroupingExpressions().isEmpty() ) { + for ( SqmRoot root : querySpec.getRootList() ) { + root.removeLeftFetchJoins(); + } + querySpec.getSelectClause().setSelection( nodeBuilder().count() ); + if ( querySpec.getFetch() == null && querySpec.getOffset() == null ) { + querySpec.setOrderByClause( null ); + } + + @SuppressWarnings("unchecked") + final SqmSelectStatement statement = (SqmSelectStatement) copy; + statement.setResultType( Long.class ); + return statement; + } + else { + final JpaSelection selection = querySpec.getSelection(); + if ( selection.isCompoundSelection() ) { + char c = 'a'; + for ( JpaSelection item: selection.getSelectionItems() ) { + item.alias( Character.toString(++c) + '_' ); + } + } + else { + selection.alias("a_"); + } + final SqmSubQuery subquery = new SqmSubQuery<>( copy, queryPart, null, nodeBuilder() ); + final SqmSelectStatement query = nodeBuilder().createQuery(Long.class); + query.from( subquery ); + query.select( nodeBuilder().count() ); + return query; + } + } + @Override public SqmSelectStatement copy(SqmCopyContext context) { final SqmSelectStatement existing = context.getCopy( this ); @@ -150,7 +200,7 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements @Override public SqmQuerySpec getQuerySpec() { - if ( querySource == SqmQuerySource.CRITERIA ) { + if ( querySource == CRITERIA ) { final SqmQueryPart queryPart = getQueryPart(); if ( queryPart instanceof SqmQuerySpec ) { return (SqmQuerySpec) queryPart; @@ -210,7 +260,7 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements @Override public Set> getSqmParameters() { - if ( querySource == SqmQuerySource.CRITERIA ) { + if ( querySource == CRITERIA ) { assert parameters == null : "SqmSelectStatement (as Criteria) should not have collected parameters"; return collectParameters( this ); } @@ -253,7 +303,7 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements // time. // // for a "finalized" set of parameters, use `#resolveParameters` instead - assert querySource == SqmQuerySource.CRITERIA; + assert querySource == CRITERIA; return getSqmParameters().stream() .filter( parameterExpression -> !( parameterExpression instanceof ValueBindJpaCriteriaParameter ) ) .collect( Collectors.toSet() ); @@ -286,47 +336,47 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements } @Override - @SuppressWarnings("unchecked") public SqmSelectStatement multiselect(List> selectionList) { if ( nodeBuilder().isJpaQueryComplianceEnabled() ) { for ( Selection selection : selectionList ) { checkSelectionIsJpaCompliant( selection ); } } + final Selection resultSelection = getResultSelection( selectionList ); + getQuerySpec().getSelectClause().setSelection( (SqmSelectableNode) resultSelection ); + return this; + } - final Selection resultSelection; + @SuppressWarnings("unchecked") + private Selection getResultSelection(List selectionList) { final Class resultType = getResultType(); - final List> selections = (List>) (List) selectionList; + final List> selections = + (List>) selectionList; if ( resultType == null || resultType == Object.class ) { - switch ( selections.size() ) { + switch ( selectionList.size() ) { case 0: { throw new IllegalArgumentException( "empty selections passed to criteria query typed as Object" ); } case 1: { - resultSelection = ( Selection ) selections.get( 0 ); - break; + return (Selection) selectionList.get( 0 ); } default: { setResultType( (Class) Object[].class ); - resultSelection = ( Selection ) nodeBuilder().array( selections ); + return (Selection) nodeBuilder().array( selections ); } } } else if ( Tuple.class.isAssignableFrom( resultType ) ) { - resultSelection = ( Selection ) nodeBuilder().tuple( selections ); + return (Selection) nodeBuilder().tuple( selections ); } else if ( resultType.isArray() ) { - resultSelection = nodeBuilder().array( resultType, selections ); + return nodeBuilder().array( resultType, selections ); } else { - resultSelection = nodeBuilder().construct( resultType, selections ); + return nodeBuilder().construct( resultType, selections ); } - - getQuerySpec().getSelectClause().setSelection( (SqmSelectableNode) resultSelection ); - - return this; } private void checkSelectionIsJpaCompliant(Selection selection) { @@ -460,51 +510,54 @@ public class SqmSelectStatement extends AbstractSqmSelectQuery implements } @Override - public JpaCriteriaQuery createCountQuery() { - final SqmCopyContext context = new NoParamSqmCopyContext() { - @Override - public boolean copyFetchedFlag() { - return false; - } - }; - final NodeBuilder nodeBuilder = nodeBuilder(); - final Set> parameters; - if ( this.parameters == null ) { - parameters = null; - } - else { - parameters = new LinkedHashSet<>( this.parameters.size() ); - for ( SqmParameter parameter : this.parameters ) { - parameters.add( parameter.copy( context ) ); - } - } - final SqmSelectStatement selectStatement = new SqmSelectStatement<>( - nodeBuilder, - copyCteStatements( context ), - Long.class, - SqmQuerySource.CRITERIA, - parameters - ); - final SqmQuerySpec querySpec = new SqmQuerySpec<>( nodeBuilder ); + public SqmSelectStatement createCountQuery() { + final SqmCopyContext copyContext = noParamCopyContext(); - final SqmSubQuery subquery = new SqmSubQuery<>( selectStatement, Tuple.class, nodeBuilder ); - final SqmQueryPart queryPart = getQueryPart().copy( context ); + final SqmQueryPart queryPart = getQueryPart().copy( copyContext ); resetSelections( queryPart ); - // Reset the if ( queryPart.getFetch() == null && queryPart.getOffset() == null ) { queryPart.setOrderByClause( null ); } + if ( queryPart.isSimpleQueryPart() ) { + for ( SqmRoot root : queryPart.getFirstQuerySpec().getRootList() ) { + root.removeLeftFetchJoins(); + } + } + + final NodeBuilder nodeBuilder = nodeBuilder(); + final Set> parameters = copyParameters( copyContext ); + final SqmSelectStatement selectStatement = new SqmSelectStatement<>( + nodeBuilder, + copyCteStatements( copyContext ), + Long.class, + CRITERIA, + parameters + ); + final SqmSubQuery subquery = new SqmSubQuery<>( selectStatement, Tuple.class, nodeBuilder ); //noinspection unchecked subquery.setQueryPart( (SqmQueryPart) queryPart ); - + final SqmQuerySpec querySpec = new SqmQuerySpec<>( nodeBuilder ); querySpec.setFromClause( new SqmFromClause( 1 ) ); querySpec.setSelectClause( new SqmSelectClause( false, 1, nodeBuilder ) ); selectStatement.setQueryPart( querySpec ); - selectStatement.select( nodeBuilder.count( new SqmStar( nodeBuilder ) ) ); + selectStatement.select( nodeBuilder.count() ); selectStatement.from( subquery ); return selectStatement; } + private Set> copyParameters(SqmCopyContext context) { + if ( parameters == null ) { + return null; + } + else { + final Set> copied = new LinkedHashSet<>( parameters.size() ); + for ( SqmParameter parameter : parameters ) { + copied.add( parameter.copy(context) ); + } + return copied; + } + } + private void resetSelections(SqmQueryPart queryPart) { if ( queryPart instanceof SqmQuerySpec ) { resetSelections( (SqmQuerySpec) queryPart ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java index 2163994409..174d03cee8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java @@ -615,14 +615,14 @@ public class SqmSubQuery extends AbstractSqmSelectQuery implements SqmSele } private void applyInferableType(Class type) { - final EntityDomainType entityDescriptor = nodeBuilder().getSessionFactory().getRuntimeMetamodels() - .getJpaMetamodel() - .findEntityType( type ); - if ( entityDescriptor != null ) { - this.expressibleType = entityDescriptor; - } - else { - this.expressibleType = nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( type ); + if ( type != null ) { + final EntityDomainType entityDescriptor = nodeBuilder().getDomainModel().findEntityType( type ); + if ( entityDescriptor != null ) { + this.expressibleType = entityDescriptor; + } + else { + this.expressibleType = nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( type ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java new file mode 100644 index 0000000000..80fe28da48 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/spi/SingleResultConsumer.java @@ -0,0 +1,47 @@ +/* + * 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.sql.results.spi; + +import org.hibernate.Incubating; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.query.SelectionQuery; +import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl; +import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; +import org.hibernate.sql.results.jdbc.spi.JdbcValues; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; + +/** + * Used beneath {@link SelectionQuery#getResultCount()}. + * + * @since 6.5 + * + * @author Gavin King + */ +@Incubating +public class SingleResultConsumer implements ResultsConsumer { + @Override + public T consume( + JdbcValues jdbcValues, + SharedSessionContractImplementor session, + JdbcValuesSourceProcessingOptions processingOptions, + JdbcValuesSourceProcessingStateStandardImpl jdbcValuesSourceProcessingState, + RowProcessingStateStandardImpl rowProcessingState, + RowReader rowReader) { + rowReader.getInitializersList().startLoading( rowProcessingState ); + rowProcessingState.next(); + final T result = rowReader.readRow( rowProcessingState, processingOptions ); + rowProcessingState.finishRowProcessing( true ); + rowReader.finishUp( jdbcValuesSourceProcessingState ); + jdbcValuesSourceProcessingState.finishUp( false ); + return result; + } + + @Override + public boolean canResultsBeCached() { + return false; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/count/CountTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/count/CountTest.java new file mode 100644 index 0000000000..49ece17d81 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/count/CountTest.java @@ -0,0 +1,152 @@ +package org.hibernate.orm.test.query.count; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.ParameterExpression; +import jakarta.persistence.criteria.Root; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SessionFactory +@DomainModel(annotatedClasses = {CountTest.Book.class, CountTest.Author.class, CountTest.Publisher.class}) +public class CountTest { + + @Test void testCount(SessionFactoryScope scope) { + scope.inTransaction(session -> session.createMutationQuery("delete Book").executeUpdate()); + scope.inTransaction(session -> { + session.persist(new Book("9781932394153", "Hibernate in Action")); + session.persist(new Book("9781617290459", "Java Persistence with Hibernate")); + }); + scope.inSession(session -> { + assertEquals(1L, + session.createSelectionQuery("from Book where title like 'Hibernate%'", Book.class) + .getResultCount()); + assertEquals(2L, + session.createSelectionQuery("from Book where title like '%Hibernate%'", Book.class) + .getResultCount()); + assertEquals(1L, + session.createSelectionQuery("select isbn, title from Book where title like 'Hibernate%'", String.class) + .getResultCount()); + assertEquals(1L, + session.createSelectionQuery("from Book where title like :title", Book.class) + .setParameter("title", "Hibernate%") + .getResultCount()); + assertEquals(0L, + session.createSelectionQuery("from Book where title like :title", Book.class) + .setParameter("title", "Jibernate%") + .getResultCount()); + assertEquals(2L, + session.createSelectionQuery("select title from Book where title like '%Hibernate' union select title from Book where title like 'Hibernate%'", String.class) + .getResultCount()); + assertEquals(2L, + session.createSelectionQuery("from Book left join fetch authors left join fetch publisher", Book.class) + .getResultCount()); + assertEquals(0L, + session.createSelectionQuery("from Book join fetch publisher", Book.class) + .getResultCount()); + }); + } + + @Test void testCountNative(SessionFactoryScope scope) { + scope.inTransaction(session -> session.createMutationQuery("delete Book").executeUpdate()); + scope.inTransaction(session -> { + session.persist(new Book("9781932394153", "Hibernate in Action")); + session.persist(new Book("9781617290459", "Java Persistence with Hibernate")); + }); + scope.inSession(session -> { + assertEquals(2L, + session.createNativeQuery("select title from books", String.class) + .setMaxResults(1) + .getResultCount()); + assertEquals(1L, + session.createNativeQuery("select title from books where title like :title", String.class) + .setParameter("title", "Hibernate%") + .getResultCount()); + assertEquals(2L, + session.createNativeQuery("select title from books", String.class) + .setMaxResults(1) + .getResultCount()); + }); + } + + @Test void testCountCriteria(SessionFactoryScope scope) { + scope.inTransaction(session -> session.createMutationQuery("delete Book").executeUpdate()); + scope.inTransaction(session -> { + session.persist(new Book("9781932394153", "Hibernate in Action")); + session.persist(new Book("9781617290459", "Java Persistence with Hibernate")); + }); + CriteriaBuilder builder = scope.getSessionFactory().getCriteriaBuilder(); + scope.inSession(session -> { + CriteriaQuery query1 = builder.createQuery(Book.class); + query1.where( builder.like( query1.from(Book.class).get("title"), "Hibernate%" ) ); + assertEquals(1L, + session.createQuery(query1) + .getResultCount()); + CriteriaQuery query2 = builder.createQuery(Book.class); + query2.from(Book.class); + assertEquals(2L, + session.createQuery(query2) + .setMaxResults(1) + .getResultCount()); + CriteriaQuery query3 = builder.createQuery(Book.class); + ParameterExpression parameter = builder.parameter(String.class); + query3.where( builder.like( query3.from(Book.class).get("title"), parameter ) ); + assertEquals(1L, + session.createQuery(query3) + .setParameter(parameter, "Hibernate%") + .getResultCount()); + CriteriaQuery query4 = builder.createQuery(Book.class); + Root book = query4.from(Book.class); + book.fetch("authors", JoinType.INNER); + assertEquals(0L, + session.createQuery(query4) + .getResultCount()); + }); + } + + @Entity(name="Book") + @Table(name = "books") + static class Book { + @Id String isbn; + String title; + + @ManyToMany + List authors; + + @ManyToOne + Publisher publisher; + + Book(String isbn, String title) { + this.isbn = isbn; + this.title = title; + } + + Book() { + } + } + + @Entity(name="Author") + @Table(name = "authors") + static class Author { + @Id String ssn; + String name; + } + + @Entity(name="Publisher") + @Table(name = "pubs") + static class Publisher { + @Id String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java index fe79235212..19ef384f08 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java @@ -66,7 +66,7 @@ public class CountQueryTests { "select e from Contact e join fetch e.alternativeContact", Contact.class ) ); - verifyCollectionCount( session, cb.createQuery( + verifyCount( session, cb.createQuery( "select e from Contact e left join fetch e.addresses", Contact.class ) ); @@ -232,19 +232,6 @@ public class CountQueryTests { assertEquals( resultList.size(), count.intValue() ); } - private void verifyCollectionCount(SessionImplementor session, JpaCriteriaQuery query) { - final List resultList = session.createQuery( query ).getResultList(); - final Long count = session.createQuery( query.createCountQuery() ).getSingleResult(); - int ormSize = 0; - for ( Contact contact : resultList ) { - ormSize++; - ormSize += Math.max( contact.getAddresses().size() - 1, 0 ); - ormSize += Math.max( contact.getPhoneNumbers().size() - 1, 0 ); - } - - assertEquals( ormSize, count.intValue() ); - } - @AfterEach public void dropTestData(SessionFactoryScope scope) { scope.inTransaction( (session) -> {