Skip to content

Commit 72fff29

Browse files
committed
HHH-19541 add createExistsQuery() to JpaCriteriaQuery
1 parent 231163f commit 72fff29

File tree

5 files changed

+91
-21
lines changed

5 files changed

+91
-21
lines changed

hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,4 +500,9 @@ public <X> JpaFunctionRoot<X> from(JpaSetReturningFunction<X> function) {
500500
public JpaCriteriaQuery<Long> createCountQuery() {
501501
return query.createCountQuery();
502502
}
503+
504+
@Override
505+
public JpaCriteriaQuery<Boolean> createExistsQuery() {
506+
return query.createExistsQuery();
507+
}
503508
}

hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCriteriaQuery.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.List;
88
import java.util.Set;
99

10+
import org.hibernate.Incubating;
1011
import org.hibernate.query.common.FetchClauseType;
1112

1213
import jakarta.persistence.criteria.CriteriaQuery;
@@ -34,6 +35,14 @@ public interface JpaCriteriaQuery<T> extends CriteriaQuery<T>, JpaQueryableCrite
3435
*/
3536
JpaCriteriaQuery<Long> createCountQuery();
3637

38+
/**
39+
* A query that returns {@code true} if this query has any results.
40+
*
41+
* @since 7.1
42+
*/
43+
@Incubating
44+
JpaCriteriaQuery<Boolean> createExistsQuery();
45+
3746
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3847
// Limit/Offset/Fetch clause
3948

hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ Map<String, SqmCteStatement<?>> getCteStatementMap() {
102102
return new LinkedHashMap<>( cteStatements );
103103
}
104104

105+
void addCteStatements(Map<String, SqmCteStatement<?>> cteStatements) {
106+
this.cteStatements.putAll( cteStatements );
107+
}
108+
105109
@Override
106110
public SqmCteStatement<?> getCteStatement(String cteLabel) {
107111
return cteStatements.get( cteLabel );
@@ -420,14 +424,11 @@ public void appendHqlString(StringBuilder hql, SqmRenderContext context) {
420424
protected Selection<? extends T> getResultSelection(Selection<?>[] selections) {
421425
final Class<T> resultType = getResultType();
422426
if ( resultType == null || resultType == Object.class ) {
423-
switch ( selections.length ) {
424-
case 0:
425-
throw new IllegalArgumentException( "Empty selections passed to criteria query typed as Object" );
426-
case 1:
427-
return (Selection<? extends T>) selections[0];
428-
default:
429-
return (Selection<? extends T>) nodeBuilder().array( selections );
430-
}
427+
return switch ( selections.length ) {
428+
case 0 -> throw new IllegalArgumentException( "Empty selections passed to criteria query typed as Object" );
429+
case 1 -> (Selection<? extends T>) selections[0];
430+
default -> (Selection<? extends T>) nodeBuilder().array( selections );
431+
};
431432
}
432433
else if ( Tuple.class.isAssignableFrom( resultType ) ) {
433434
return (Selection<? extends T>) nodeBuilder().tuple( selections );

hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,10 +537,10 @@ public SqmSelectStatement<Long> createCountQuery() {
537537
if ( querySpec.getFetch() == null && querySpec.getOffset() == null ) {
538538
querySpec.setOrderByClause( null );
539539
}
540-
541540
return (SqmSelectStatement<Long>) copy;
542541
}
543542
else {
543+
//TODO: do some deeper analysis for unions (simplify their select lists)
544544
aliasSelections( queryPart );
545545
final SqmSubQuery<?> subquery = new SqmSubQuery<>( copy, queryPart, null, nodeBuilder() );
546546
final SqmSelectStatement<Long> query = nodeBuilder().createQuery( Long.class );
@@ -553,6 +553,35 @@ public SqmSelectStatement<Long> createCountQuery() {
553553
}
554554
}
555555

556+
@Override
557+
public SqmSelectStatement<Boolean> createExistsQuery() {
558+
final SqmSelectStatement<?> copy = createCopy( noParamCopyContext(), Object.class );
559+
final SqmQueryPart<?> queryPart = copy.getQueryPart();
560+
//TODO: detect queries with no 'group by', but aggregate functions
561+
// in 'select' list (we don't even need to hit the database to
562+
// know they return exactly one row)
563+
if ( queryPart.isSimpleQueryPart() ) {
564+
final SqmQuerySpec<?> querySpec = (SqmQuerySpec<?>) queryPart;
565+
querySpec.setDistinct( false );
566+
if ( querySpec.getGroupingExpressions().isEmpty() ) {
567+
for ( SqmRoot<?> root : querySpec.getRootList() ) {
568+
root.removeLeftFetchJoins();
569+
}
570+
querySpec.getSelectClause().setSelection( nodeBuilder().literal( 1 ) );
571+
}
572+
}
573+
//TODO: do some deeper analysis for unions (simplify their select lists)
574+
aliasSelections( queryPart );
575+
final SqmSubQuery<?> subquery = new SqmSubQuery<>( copy, queryPart, null, nodeBuilder() );
576+
final SqmSelectStatement<Boolean> query = nodeBuilder().createQuery( Boolean.class );
577+
query.select( nodeBuilder().exists( subquery ) );
578+
if ( subquery.getFetch() == null && subquery.getOffset() == null ) {
579+
subquery.getQueryPart().setOrderByClause( null );
580+
}
581+
query.addCteStatements( getCteStatementMap() );
582+
return query;
583+
}
584+
556585
private <S> void aliasSelections(SqmQueryPart<S> queryPart) {
557586
if ( queryPart.isSimpleQueryPart() ) {
558587
final SqmQuerySpec<S> querySpec = queryPart.getFirstQuerySpec();

hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.List;
99

1010
import org.hibernate.annotations.Imported;
11+
import org.hibernate.dialect.SybaseDialect;
1112
import org.hibernate.engine.spi.SessionImplementor;
1213
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
1314
import org.hibernate.query.criteria.JpaCriteriaQuery;
@@ -43,6 +44,7 @@
4344
import jakarta.persistence.criteria.Root;
4445

4546
import static org.junit.jupiter.api.Assertions.assertEquals;
47+
import static org.junit.jupiter.api.Assertions.assertTrue;
4648
import static org.junit.jupiter.api.Assertions.fail;
4749

4850
/**
@@ -72,10 +74,17 @@ public void testForHHH17967(SessionFactoryScope scope) {
7274
JpaCriteriaQuery<Contract> cq = cb.createQuery( Contract.class );
7375
Root<Contract> root = cq.from( Contract.class );
7476
cq.select( root );
75-
TypedQuery<Long> query = session.createQuery( cq.createCountQuery() );
77+
TypedQuery<Long> countQuery = session.createQuery( cq.createCountQuery() );
7678
try {
7779
// Leads to NPE on pre-6.5 versions
78-
query.getSingleResult();
80+
countQuery.getSingleResult();
81+
}
82+
catch (Exception e) {
83+
fail( e );
84+
}
85+
TypedQuery<Boolean> existsQuery = session.createQuery( cq.createExistsQuery() );
86+
try {
87+
existsQuery.getSingleResult();
7988
}
8089
catch (Exception e) {
8190
fail( e );
@@ -95,10 +104,17 @@ public void testForHHH18850(SessionFactoryScope scope) {
95104
Root<Contract> root = cq.from( Contract.class );
96105
cq.select( root );
97106
cq.orderBy( cb.asc( root.get( "customerName" ) ) );
98-
TypedQuery<Long> query = session.createQuery( cq.createCountQuery() );
107+
TypedQuery<Long> countQuery = session.createQuery( cq.createCountQuery() );
99108
try {
100109
// Leads to NPE on pre-6.5 versions
101-
query.getSingleResult();
110+
countQuery.getSingleResult();
111+
}
112+
catch (Exception e) {
113+
fail( e );
114+
}
115+
TypedQuery<Boolean> existsQuery = session.createQuery( cq.createExistsQuery() );
116+
try {
117+
existsQuery.getSingleResult();
102118
}
103119
catch (Exception e) {
104120
fail( e );
@@ -114,10 +130,17 @@ public void testForHHH18850(SessionFactoryScope scope) {
114130
Root<Contract> root = cq.from( Contract.class );
115131
cq.select( root );
116132
cq.orderBy( cb.desc( root.get( "customerName" ) ) );
117-
TypedQuery<Long> query = session.createQuery( cq.createCountQuery() );
133+
TypedQuery<Long> countQuery = session.createQuery( cq.createCountQuery() );
118134
try {
119135
// Leads to NPE on pre-6.5 versions
120-
query.getSingleResult();
136+
countQuery.getSingleResult();
137+
}
138+
catch (Exception e) {
139+
fail( e );
140+
}
141+
TypedQuery<Boolean> existsQuery = session.createQuery( cq.createExistsQuery() );
142+
try {
143+
existsQuery.getSingleResult();
121144
}
122145
catch (Exception e) {
123146
fail( e );
@@ -257,8 +280,10 @@ public void testDistinctDynamicInstantiation(SessionFactoryScope scope) {
257280
)
258281
).distinct( true );
259282
final Long count = session.createQuery( cq.createCountQuery() ).getSingleResult();
283+
final Boolean exists = session.createQuery( cq.createExistsQuery() ).getSingleResult();
260284
final List<Tuple> resultList = session.createQuery( cq ).getResultList();
261285
assertEquals( 1L, count );
286+
assertTrue( exists );
262287
assertEquals( resultList.size(), count.intValue() );
263288
} );
264289
}
@@ -278,6 +303,10 @@ public void testUnionQuery(SessionFactoryScope scope) {
278303
cq2.select( root2.get( "name" ).get( "first" ) ).where( cb.equal( root2.get( "id" ), 2 ) );
279304

280305
final JpaCriteriaQuery<String> union = cb.union( cq1, cq2 );
306+
if ( !(scope.getSessionFactory().getJdbcServices().getDialect() instanceof SybaseDialect) ) {
307+
final Boolean exists = session.createQuery( union.createExistsQuery() ).getSingleResult();
308+
assertTrue( exists );
309+
}
281310
final Long count = session.createQuery( union.createCountQuery() ).getSingleResult();
282311
final List<String> resultList = session.createQuery( union ).getResultList();
283312
assertEquals( 2L, count );
@@ -390,16 +419,13 @@ private <T> void verifyCount(SessionImplementor session, JpaCriteriaQuery<?> que
390419
final List<?> resultList = session.createQuery( query ).getResultList();
391420
final Long count = session.createQuery( query.createCountQuery() ).getSingleResult();
392421
assertEquals( resultList.size(), count.intValue() );
422+
final Boolean exists = session.createQuery( query.createExistsQuery() ).getSingleResult();
423+
assertEquals( !resultList.isEmpty(), exists );
393424
}
394425

395426
@AfterEach
396427
public void dropTestData(SessionFactoryScope scope) {
397-
scope.inTransaction( (session) -> {
398-
session.createMutationQuery( "update Contact set alternativeContact = null" ).executeUpdate();
399-
session.createMutationQuery( "delete Contact" ).executeUpdate();
400-
session.createMutationQuery( "delete ChildEntity" ).executeUpdate();
401-
session.createMutationQuery( "delete ParentEntity" ).executeUpdate();
402-
} );
428+
scope.getSessionFactory().getSchemaManager().truncate();
403429
}
404430

405431
@MappedSuperclass

0 commit comments

Comments
 (0)