From 027b3aabf29dabaf68440fb5f566396fd6d4e3ca Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sat, 14 Jun 2025 12:40:20 +0200 Subject: [PATCH] HHH-19541 add createExistsQuery() to JpaCriteriaQuery --- .../query/criteria/CriteriaDefinition.java | 5 ++ .../query/criteria/JpaCriteriaQuery.java | 9 ++++ .../tree/select/AbstractSqmSelectQuery.java | 17 ++++--- .../sqm/tree/select/SqmSelectStatement.java | 31 +++++++++++- .../test/query/criteria/CountQueryTests.java | 50 ++++++++++++++----- 5 files changed, 91 insertions(+), 21 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java index 93531db20ba4..7105a9213e2d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java @@ -500,4 +500,9 @@ public JpaFunctionRoot from(JpaSetReturningFunction function) { public JpaCriteriaQuery createCountQuery() { return query.createCountQuery(); } + + @Override + public JpaCriteriaQuery createExistsQuery() { + return query.createExistsQuery(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaQuery.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaQuery.java index e87c0fbb7a81..b4b862dba9f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaQuery.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Set; +import org.hibernate.Incubating; import org.hibernate.query.common.FetchClauseType; import jakarta.persistence.criteria.CriteriaQuery; @@ -34,6 +35,14 @@ public interface JpaCriteriaQuery extends CriteriaQuery, JpaQueryableCrite */ JpaCriteriaQuery createCountQuery(); + /** + * A query that returns {@code true} if this query has any results. + * + * @since 7.1 + */ + @Incubating + JpaCriteriaQuery createExistsQuery(); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Limit/Offset/Fetch clause diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java index 347938dd3d15..b272176e123b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java @@ -102,6 +102,10 @@ Map> getCteStatementMap() { return new LinkedHashMap<>( cteStatements ); } + void addCteStatements(Map> cteStatements) { + this.cteStatements.putAll( cteStatements ); + } + @Override public SqmCteStatement getCteStatement(String cteLabel) { return cteStatements.get( cteLabel ); @@ -420,14 +424,11 @@ public void appendHqlString(StringBuilder hql, SqmRenderContext context) { protected Selection getResultSelection(Selection[] selections) { final Class resultType = getResultType(); if ( resultType == null || resultType == Object.class ) { - switch ( selections.length ) { - case 0: - throw new IllegalArgumentException( "Empty selections passed to criteria query typed as Object" ); - case 1: - return (Selection) selections[0]; - default: - return (Selection) nodeBuilder().array( selections ); - } + return switch ( selections.length ) { + case 0 -> throw new IllegalArgumentException( "Empty selections passed to criteria query typed as Object" ); + case 1 -> (Selection) selections[0]; + default -> (Selection) nodeBuilder().array( selections ); + }; } else if ( Tuple.class.isAssignableFrom( resultType ) ) { return (Selection) nodeBuilder().tuple( selections ); 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 9a6630ae57d9..719f284716a5 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 @@ -537,10 +537,10 @@ public SqmSelectStatement createCountQuery() { if ( querySpec.getFetch() == null && querySpec.getOffset() == null ) { querySpec.setOrderByClause( null ); } - return (SqmSelectStatement) copy; } else { + //TODO: do some deeper analysis for unions (simplify their select lists) aliasSelections( queryPart ); final SqmSubQuery subquery = new SqmSubQuery<>( copy, queryPart, null, nodeBuilder() ); final SqmSelectStatement query = nodeBuilder().createQuery( Long.class ); @@ -553,6 +553,35 @@ public SqmSelectStatement createCountQuery() { } } + @Override + public SqmSelectStatement createExistsQuery() { + final SqmSelectStatement copy = createCopy( noParamCopyContext(), Object.class ); + 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() ) { + final SqmQuerySpec querySpec = (SqmQuerySpec) queryPart; + querySpec.setDistinct( false ); + if ( querySpec.getGroupingExpressions().isEmpty() ) { + for ( SqmRoot root : querySpec.getRootList() ) { + root.removeLeftFetchJoins(); + } + querySpec.getSelectClause().setSelection( nodeBuilder().literal( 1 ) ); + } + } + //TODO: do some deeper analysis for unions (simplify their select lists) + aliasSelections( queryPart ); + final SqmSubQuery subquery = new SqmSubQuery<>( copy, queryPart, null, nodeBuilder() ); + final SqmSelectStatement query = nodeBuilder().createQuery( Boolean.class ); + query.select( nodeBuilder().exists( subquery ) ); + if ( subquery.getFetch() == null && subquery.getOffset() == null ) { + subquery.getQueryPart().setOrderByClause( null ); + } + query.addCteStatements( getCteStatementMap() ); + return query; + } + private void aliasSelections(SqmQueryPart queryPart) { if ( queryPart.isSimpleQueryPart() ) { final SqmQuerySpec querySpec = queryPart.getFirstQuerySpec(); 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 c78c0e1a58ad..5a8268494759 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 @@ -8,6 +8,7 @@ import java.util.List; import org.hibernate.annotations.Imported; +import org.hibernate.dialect.SybaseDialect; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaCriteriaQuery; @@ -43,6 +44,7 @@ import jakarta.persistence.criteria.Root; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** @@ -72,10 +74,17 @@ public void testForHHH17967(SessionFactoryScope scope) { JpaCriteriaQuery cq = cb.createQuery( Contract.class ); Root root = cq.from( Contract.class ); cq.select( root ); - TypedQuery query = session.createQuery( cq.createCountQuery() ); + TypedQuery countQuery = session.createQuery( cq.createCountQuery() ); try { // Leads to NPE on pre-6.5 versions - query.getSingleResult(); + countQuery.getSingleResult(); + } + catch (Exception e) { + fail( e ); + } + TypedQuery existsQuery = session.createQuery( cq.createExistsQuery() ); + try { + existsQuery.getSingleResult(); } catch (Exception e) { fail( e ); @@ -95,10 +104,17 @@ public void testForHHH18850(SessionFactoryScope scope) { Root root = cq.from( Contract.class ); cq.select( root ); cq.orderBy( cb.asc( root.get( "customerName" ) ) ); - TypedQuery query = session.createQuery( cq.createCountQuery() ); + TypedQuery countQuery = session.createQuery( cq.createCountQuery() ); try { // Leads to NPE on pre-6.5 versions - query.getSingleResult(); + countQuery.getSingleResult(); + } + catch (Exception e) { + fail( e ); + } + TypedQuery existsQuery = session.createQuery( cq.createExistsQuery() ); + try { + existsQuery.getSingleResult(); } catch (Exception e) { fail( e ); @@ -114,10 +130,17 @@ public void testForHHH18850(SessionFactoryScope scope) { Root root = cq.from( Contract.class ); cq.select( root ); cq.orderBy( cb.desc( root.get( "customerName" ) ) ); - TypedQuery query = session.createQuery( cq.createCountQuery() ); + TypedQuery countQuery = session.createQuery( cq.createCountQuery() ); try { // Leads to NPE on pre-6.5 versions - query.getSingleResult(); + countQuery.getSingleResult(); + } + catch (Exception e) { + fail( e ); + } + TypedQuery existsQuery = session.createQuery( cq.createExistsQuery() ); + try { + existsQuery.getSingleResult(); } catch (Exception e) { fail( e ); @@ -257,8 +280,10 @@ public void testDistinctDynamicInstantiation(SessionFactoryScope scope) { ) ).distinct( true ); final Long count = session.createQuery( cq.createCountQuery() ).getSingleResult(); + final Boolean exists = session.createQuery( cq.createExistsQuery() ).getSingleResult(); final List resultList = session.createQuery( cq ).getResultList(); assertEquals( 1L, count ); + assertTrue( exists ); assertEquals( resultList.size(), count.intValue() ); } ); } @@ -278,6 +303,10 @@ public void testUnionQuery(SessionFactoryScope scope) { cq2.select( root2.get( "name" ).get( "first" ) ).where( cb.equal( root2.get( "id" ), 2 ) ); final JpaCriteriaQuery union = cb.union( cq1, cq2 ); + if ( !(scope.getSessionFactory().getJdbcServices().getDialect() instanceof SybaseDialect) ) { + final Boolean exists = session.createQuery( union.createExistsQuery() ).getSingleResult(); + assertTrue( exists ); + } final Long count = session.createQuery( union.createCountQuery() ).getSingleResult(); final List resultList = session.createQuery( union ).getResultList(); assertEquals( 2L, count ); @@ -390,16 +419,13 @@ private void verifyCount(SessionImplementor session, JpaCriteriaQuery que final List resultList = session.createQuery( query ).getResultList(); final Long count = session.createQuery( query.createCountQuery() ).getSingleResult(); assertEquals( resultList.size(), count.intValue() ); + final Boolean exists = session.createQuery( query.createExistsQuery() ).getSingleResult(); + assertEquals( !resultList.isEmpty(), exists ); } @AfterEach public void dropTestData(SessionFactoryScope scope) { - scope.inTransaction( (session) -> { - session.createMutationQuery( "update Contact set alternativeContact = null" ).executeUpdate(); - session.createMutationQuery( "delete Contact" ).executeUpdate(); - session.createMutationQuery( "delete ChildEntity" ).executeUpdate(); - session.createMutationQuery( "delete ParentEntity" ).executeUpdate(); - } ); + scope.getSessionFactory().getSchemaManager().truncate(); } @MappedSuperclass