Skip to content

Commit 17ceb80

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

File tree

5 files changed

+85
-21
lines changed

5 files changed

+85
-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: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,6 @@ 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 {
@@ -553,6 +552,33 @@ public SqmSelectStatement<Long> createCountQuery() {
553552
}
554553
}
555554

555+
@Override
556+
public SqmSelectStatement<Boolean> createExistsQuery() {
557+
final SqmSelectStatement<?> copy = createCopy( noParamCopyContext(), Object.class );
558+
final SqmQueryPart<?> queryPart = copy.getQueryPart();
559+
final SqmQuerySpec<?> querySpec;
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+
&& !( querySpec = (SqmQuerySpec<?>) queryPart ).isDistinct()
565+
&& querySpec.getGroupingExpressions().isEmpty() ) {
566+
for ( SqmRoot<?> root : querySpec.getRootList() ) {
567+
root.removeLeftFetchJoins();
568+
}
569+
querySpec.getSelectClause().setSelection( nodeBuilder().literal(1) );
570+
}
571+
aliasSelections( queryPart );
572+
final SqmSubQuery<?> subquery = new SqmSubQuery<>( copy, queryPart, null, nodeBuilder() );
573+
final SqmSelectStatement<Boolean> query = nodeBuilder().createQuery( Boolean.class );
574+
query.select( nodeBuilder().exists( subquery ) );
575+
if ( subquery.getFetch() == null && subquery.getOffset() == null ) {
576+
subquery.getQueryPart().setOrderByClause( null );
577+
}
578+
query.addCteStatements( getCteStatementMap() );
579+
return query;
580+
}
581+
556582
private <S> void aliasSelections(SqmQueryPart<S> queryPart) {
557583
if ( queryPart.isSimpleQueryPart() ) {
558584
final SqmQuerySpec<S> querySpec = queryPart.getFirstQuerySpec();

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

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import jakarta.persistence.criteria.Root;
4444

4545
import static org.junit.jupiter.api.Assertions.assertEquals;
46+
import static org.junit.jupiter.api.Assertions.assertTrue;
4647
import static org.junit.jupiter.api.Assertions.fail;
4748

4849
/**
@@ -72,10 +73,17 @@ public void testForHHH17967(SessionFactoryScope scope) {
7273
JpaCriteriaQuery<Contract> cq = cb.createQuery( Contract.class );
7374
Root<Contract> root = cq.from( Contract.class );
7475
cq.select( root );
75-
TypedQuery<Long> query = session.createQuery( cq.createCountQuery() );
76+
TypedQuery<Long> countQuery = session.createQuery( cq.createCountQuery() );
7677
try {
7778
// Leads to NPE on pre-6.5 versions
78-
query.getSingleResult();
79+
countQuery.getSingleResult();
80+
}
81+
catch (Exception e) {
82+
fail( e );
83+
}
84+
TypedQuery<Boolean> existsQuery = session.createQuery( cq.createExistsQuery() );
85+
try {
86+
existsQuery.getSingleResult();
7987
}
8088
catch (Exception e) {
8189
fail( e );
@@ -95,10 +103,17 @@ public void testForHHH18850(SessionFactoryScope scope) {
95103
Root<Contract> root = cq.from( Contract.class );
96104
cq.select( root );
97105
cq.orderBy( cb.asc( root.get( "customerName" ) ) );
98-
TypedQuery<Long> query = session.createQuery( cq.createCountQuery() );
106+
TypedQuery<Long> countQuery = session.createQuery( cq.createCountQuery() );
99107
try {
100108
// Leads to NPE on pre-6.5 versions
101-
query.getSingleResult();
109+
countQuery.getSingleResult();
110+
}
111+
catch (Exception e) {
112+
fail( e );
113+
}
114+
TypedQuery<Boolean> existsQuery = session.createQuery( cq.createExistsQuery() );
115+
try {
116+
existsQuery.getSingleResult();
102117
}
103118
catch (Exception e) {
104119
fail( e );
@@ -114,10 +129,17 @@ public void testForHHH18850(SessionFactoryScope scope) {
114129
Root<Contract> root = cq.from( Contract.class );
115130
cq.select( root );
116131
cq.orderBy( cb.desc( root.get( "customerName" ) ) );
117-
TypedQuery<Long> query = session.createQuery( cq.createCountQuery() );
132+
TypedQuery<Long> countQuery = session.createQuery( cq.createCountQuery() );
118133
try {
119134
// Leads to NPE on pre-6.5 versions
120-
query.getSingleResult();
135+
countQuery.getSingleResult();
136+
}
137+
catch (Exception e) {
138+
fail( e );
139+
}
140+
TypedQuery<Boolean> existsQuery = session.createQuery( cq.createExistsQuery() );
141+
try {
142+
existsQuery.getSingleResult();
121143
}
122144
catch (Exception e) {
123145
fail( e );
@@ -257,8 +279,10 @@ public void testDistinctDynamicInstantiation(SessionFactoryScope scope) {
257279
)
258280
).distinct( true );
259281
final Long count = session.createQuery( cq.createCountQuery() ).getSingleResult();
282+
final Boolean exists = session.createQuery( cq.createExistsQuery() ).getSingleResult();
260283
final List<Tuple> resultList = session.createQuery( cq ).getResultList();
261284
assertEquals( 1L, count );
285+
assertTrue( exists );
262286
assertEquals( resultList.size(), count.intValue() );
263287
} );
264288
}
@@ -279,8 +303,10 @@ public void testUnionQuery(SessionFactoryScope scope) {
279303

280304
final JpaCriteriaQuery<String> union = cb.union( cq1, cq2 );
281305
final Long count = session.createQuery( union.createCountQuery() ).getSingleResult();
306+
final Boolean exists = session.createQuery( union.createExistsQuery() ).getSingleResult();
282307
final List<String> resultList = session.createQuery( union ).getResultList();
283308
assertEquals( 2L, count );
309+
assertTrue( exists );
284310
assertEquals( resultList.size(), count.intValue() );
285311
} );
286312
}
@@ -390,16 +416,13 @@ private <T> void verifyCount(SessionImplementor session, JpaCriteriaQuery<?> que
390416
final List<?> resultList = session.createQuery( query ).getResultList();
391417
final Long count = session.createQuery( query.createCountQuery() ).getSingleResult();
392418
assertEquals( resultList.size(), count.intValue() );
419+
final Boolean exists = session.createQuery( query.createExistsQuery() ).getSingleResult();
420+
assertEquals( !resultList.isEmpty(), exists );
393421
}
394422

395423
@AfterEach
396424
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-
} );
425+
scope.getSessionFactory().getSchemaManager().truncate();
403426
}
404427

405428
@MappedSuperclass

0 commit comments

Comments
 (0)