Skip to content

Commit fc33eea

Browse files
authored
#1528: Hibernate CTE support (#1529)
Extending AbstractHibernateQuery to support simple Common Table Expressions.
2 parents 7ed52db + 7a80ebe commit fc33eea

8 files changed

Lines changed: 386 additions & 7 deletions

File tree

docs/tutorials/hibernate.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,102 @@ List results = hibernateQuery.list();
154154
This returns a Hibernate `org.hibernate.query.Query` rather than a JPA
155155
`jakarta.persistence.Query`.
156156

157+
## Common Table Expressions
158+
159+
`HibernateQuery` supports Common Table Expressions (CTEs) via the `with()`
160+
method family. CTEs define named temporary result sets that can be referenced
161+
in the main query. This feature requires Hibernate 6.5 or later and is not
162+
available on `JPAQuery`.
163+
164+
| Method | Description |
165+
|---|---|
166+
| `with(alias, subquery)` | Define a CTE |
167+
| `withMaterializedHint(alias, subquery)` | Define a CTE with `MATERIALIZED` hint |
168+
| `withNotMaterializedHint(alias, subquery)` | Define a CTE with `NOT MATERIALIZED` hint |
169+
170+
### Simple CTE with Join
171+
172+
Define a CTE that selects a specific cat's weight, then join against it to
173+
find lighter cats:
174+
175+
```java
176+
QCat cat = QCat.cat;
177+
QCat felix = new QCat("felix");
178+
179+
Cat result = new HibernateQuery<Cat>(session)
180+
.withNotMaterializedHint(felix,
181+
JPAExpressions.select(cat.bodyWeight.as(felix.bodyWeight))
182+
.from(cat)
183+
.where(cat.name.eq("Felix")))
184+
.select(cat)
185+
.from(felix)
186+
.join(cat).on(cat.bodyWeight.lt(felix.bodyWeight))
187+
.orderBy(cat.bodyWeight.desc())
188+
.limit(1)
189+
.fetchOne();
190+
```
191+
192+
Generated HQL:
193+
194+
```
195+
with
196+
felix as not materialized (select cat.bodyWeight as bodyWeight
197+
from Cat cat
198+
where cat.name = ?1)
199+
select cat
200+
from Cat cat, felix felix
201+
where cat.bodyWeight > felix.bodyWeight
202+
```
203+
204+
### CTE with Custom Column
205+
206+
Use `Expressions.numberPath()` to create custom column references within a CTE:
207+
208+
```java
209+
QCat cat = QCat.cat;
210+
QCat avgCat = new QCat("avgcat");
211+
NumberPath<Double> avgWeight = Expressions.numberPath(Double.class, avgCat, "avgweight");
212+
213+
List<Cat> results = new HibernateQuery<Cat>(session)
214+
.with(avgCat,
215+
JPAExpressions.select(cat.bodyWeight.avg().as(avgWeight))
216+
.from(cat))
217+
.select(cat)
218+
.from(cat, avgCat)
219+
.orderBy(cat.bodyWeight.subtract(avgWeight).abs().asc(), cat.id.asc())
220+
.fetch();
221+
```
222+
223+
### Multiple CTEs
224+
225+
Chain `.with()` calls to define multiple CTEs. Later CTEs can reference earlier
226+
ones:
227+
228+
```java
229+
QCat cat = QCat.cat;
230+
QCat felix = new QCat("felix");
231+
QCat felixMates = new QCat("felixMates");
232+
233+
List<Integer> results = new HibernateQuery<Integer>(session)
234+
.with(felix,
235+
JPAExpressions.select(cat.id.as(felix.id))
236+
.from(cat)
237+
.where(cat.name.eq("Felix")))
238+
.with(felixMates,
239+
JPAExpressions.select(cat.id.as(cat.id))
240+
.from(cat)
241+
.innerJoin(felix).on(cat.mate.id.eq(felix.id)))
242+
.select(felixMates.id)
243+
.from(felixMates)
244+
.fetch();
245+
```
246+
247+
{: .note }
248+
CTE aliases reuse existing Q-types (e.g., `new QCat("felix")`) to define CTE
249+
columns. Use `as()` in projections to map columns from the subquery to the CTE
250+
alias fields. The `JPAExpressions` factory is used for CTE subqueries, same as
251+
for standard JPQL subqueries.
252+
157253
## Native SQL with Hibernate
158254

159255
Use `HibernateSQLQuery` to run native SQL through a Hibernate `Session`:
@@ -173,6 +269,7 @@ for more details on native SQL query patterns.
173269
|---|---|---|
174270
| Underlying API | JPA EntityManager | Hibernate Session |
175271
| Query factory | `JPAQueryFactory` | `HibernateQueryFactory` |
272+
| Common Table Expressions | Not available | `with()`, `withMaterializedHint()`, `withNotMaterializedHint()` |
176273
| Query caching | Not available | `setCacheable()`, `setCacheRegion()` |
177274
| Read-only mode | Not available | `setReadOnly()` |
178275
| SQL comments | Not available | `setComment()` |

docs/tutorials/jpa.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ The `JPAAnnotationProcessor` finds domain types annotated with the
5858
If you use Hibernate annotations in your domain types, use the processor
5959
`com.querydsl.apt.hibernate.HibernateAnnotationProcessor` instead. See the
6060
[Hibernate tutorial]({{ site.baseurl }}/tutorials/hibernate) for
61-
Hibernate-specific features such as query caching and read-only mode.
61+
Hibernate-specific features such as Common Table Expressions, query caching,
62+
and read-only mode.
6263

6364
Run `mvn clean install` and your query types will be generated into
6465
`target/generated-sources/java`.
@@ -155,8 +156,9 @@ Both `JPAQuery` and `HibernateQuery` implement the `JPQLQuery` interface.
155156

156157
For the examples in this chapter, queries are created via a `JPAQueryFactory`
157158
instance. `JPAQueryFactory` should be the preferred option for obtaining
158-
`JPAQuery` instances. For the Hibernate API, `HibernateQueryFactory` can be used. For
159-
Hibernate-specific features, see the
159+
`JPAQuery` instances. For the Hibernate API, `HibernateQueryFactory` can be
160+
used. For Hibernate-specific features such as CTEs, query caching, and
161+
read-only mode, see the
160162
[Hibernate tutorial]({{ site.baseurl }}/tutorials/hibernate).
161163

162164
To retrieve the customer with the first name Bob:

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLSerializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private String getEntityName(Class<?> clazz) {
135135
}
136136
}
137137

138-
private void handleJoinTarget(JoinExpression je) {
138+
protected void handleJoinTarget(JoinExpression je) {
139139
// type specifier
140140
if (je.getTarget() instanceof EntityPath<?>) {
141141
final EntityPath<?> pe = (EntityPath<?>) je.getTarget();

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/AbstractHibernateQuery.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,19 @@
1717
import com.querydsl.core.DefaultQueryMetadata;
1818
import com.querydsl.core.NonUniqueResultException;
1919
import com.querydsl.core.QueryException;
20+
import com.querydsl.core.QueryFlag;
2021
import com.querydsl.core.QueryMetadata;
2122
import com.querydsl.core.QueryModifiers;
2223
import com.querydsl.core.QueryResults;
24+
import com.querydsl.core.types.ConstantImpl;
2325
import com.querydsl.core.types.Expression;
26+
import com.querydsl.core.types.ExpressionUtils;
2427
import com.querydsl.core.types.FactoryExpression;
2528
import com.querydsl.core.types.Path;
29+
import com.querydsl.core.types.SubQueryExpression;
2630
import com.querydsl.jpa.FactoryExpressionTransformer;
2731
import com.querydsl.jpa.HQLTemplates;
2832
import com.querydsl.jpa.JPAQueryBase;
29-
import com.querydsl.jpa.JPQLSerializer;
3033
import com.querydsl.jpa.JPQLTemplates;
3134
import com.querydsl.jpa.ScrollableResultsIterator;
3235
import java.util.HashMap;
@@ -95,6 +98,29 @@ public long fetchCount() {
9598
}
9699
}
97100

101+
public Q with(Path<?> alias, SubQueryExpression<?> query) {
102+
return with(alias, null, query);
103+
}
104+
105+
public Q withMaterializedHint(Path<?> alias, SubQueryExpression<?> query) {
106+
return with(alias, true, query);
107+
}
108+
109+
public Q withNotMaterializedHint(Path<?> alias, SubQueryExpression<?> query) {
110+
return with(alias, false, query);
111+
}
112+
113+
public Q with(Path<?> alias, Boolean materialized, SubQueryExpression<?> query) {
114+
Expression<?> expr =
115+
ExpressionUtils.operation(
116+
alias.getType(),
117+
HQLOps.WITH,
118+
alias,
119+
materialized != null ? ConstantImpl.create(materialized) : null,
120+
query);
121+
return queryMixin.addFlag(new QueryFlag(QueryFlag.Position.WITH, expr));
122+
}
123+
98124
/**
99125
* Expose the original Hibernate query for the given projection
100126
*
@@ -348,8 +374,8 @@ public T fetchOne() throws NonUniqueResultException {
348374
}
349375

350376
@Override
351-
protected JPQLSerializer createSerializer() {
352-
return new JPQLSerializer(getTemplates());
377+
protected HQLSerializer createSerializer() {
378+
return new HQLSerializer(getTemplates());
353379
}
354380

355381
protected void clone(Q query) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.querydsl.jpa.hibernate;
2+
3+
import com.querydsl.core.types.Operator;
4+
5+
public enum HQLOps implements Operator {
6+
WITH(Object.class);
7+
8+
private final Class<?> type;
9+
10+
HQLOps(Class<?> type) {
11+
this.type = type;
12+
}
13+
14+
@Override
15+
public Class<?> getType() {
16+
return type;
17+
}
18+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.querydsl.jpa.hibernate;
2+
3+
import com.querydsl.core.JoinExpression;
4+
import com.querydsl.core.QueryFlag;
5+
import com.querydsl.core.QueryMetadata;
6+
import com.querydsl.core.types.ConstantImpl;
7+
import com.querydsl.core.types.Expression;
8+
import com.querydsl.core.types.Operator;
9+
import com.querydsl.core.types.Path;
10+
import com.querydsl.jpa.JPQLSerializer;
11+
import com.querydsl.jpa.JPQLTemplates;
12+
import jakarta.persistence.EntityManager;
13+
import java.util.ArrayList;
14+
import java.util.HashSet;
15+
import java.util.List;
16+
import java.util.Set;
17+
import org.jetbrains.annotations.Nullable;
18+
19+
public class HQLSerializer extends JPQLSerializer {
20+
21+
protected final Set<Path<?>> withAliases = new HashSet<>();
22+
23+
public HQLSerializer(JPQLTemplates templates) {
24+
super(templates);
25+
}
26+
27+
public HQLSerializer(JPQLTemplates templates, EntityManager em) {
28+
super(templates, em);
29+
}
30+
31+
@Override
32+
public void serialize(QueryMetadata metadata, boolean forCountRow, @Nullable String projection) {
33+
final Set<QueryFlag> flags = metadata.getFlags();
34+
final var hasFlags = !flags.isEmpty();
35+
36+
if (hasFlags) {
37+
List<Expression<?>> withFlags = new ArrayList<>();
38+
for (QueryFlag flag : flags) {
39+
if (flag.getPosition() == QueryFlag.Position.WITH) {
40+
withFlags.add(flag.getFlag());
41+
}
42+
}
43+
if (!withFlags.isEmpty()) {
44+
append("with\n");
45+
handle(",\n", withFlags);
46+
append("\n");
47+
}
48+
}
49+
50+
super.serialize(metadata, forCountRow, projection);
51+
}
52+
53+
@Override
54+
protected void visitOperation(
55+
Class<?> type, Operator operator, List<? extends Expression<?>> args) {
56+
if (operator == HQLOps.WITH && args.size() == 3 && args.get(0) instanceof Path<?> alias) {
57+
handle(alias);
58+
withAliases.add(alias);
59+
append(" as ");
60+
if (args.get(1) instanceof ConstantImpl<?> materializedParam
61+
&& materializedParam.getConstant() instanceof Boolean materialized) {
62+
if (!materialized) {
63+
append("not ");
64+
}
65+
append("materialized ");
66+
}
67+
handle(args.get(2));
68+
} else {
69+
super.visitOperation(type, operator, args);
70+
}
71+
}
72+
73+
@Override
74+
protected void handleJoinTarget(JoinExpression je) {
75+
if (je.getTarget() instanceof Path<?> pe && withAliases.contains(pe)) {
76+
append(pe.getMetadata().getName()).append(" ");
77+
handle(je.getTarget());
78+
} else {
79+
super.handleJoinTarget(je);
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)