Skip to content

Commit 1fcdb17

Browse files
committed
#1528: Hibernate CTE support
Extending AbstractHibernateQuery to support simple Common Table Expressions.
1 parent 3ed9937 commit 1fcdb17

5 files changed

Lines changed: 269 additions & 4 deletions

File tree

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: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@
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;
35+
import com.querydsl.sql.SQLOps;
3236
import java.util.HashMap;
3337
import java.util.List;
3438
import java.util.Map;
@@ -95,6 +99,29 @@ public long fetchCount() {
9599
}
96100
}
97101

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

350377
@Override
351-
protected JPQLSerializer createSerializer() {
352-
return new JPQLSerializer(getTemplates());
378+
protected HQLSerializer createSerializer() {
379+
return new HQLSerializer(getTemplates());
353380
}
354381

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

querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateBase.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import com.querydsl.core.testutil.ExcludeIn;
2525
import com.querydsl.core.types.EntityPath;
2626
import com.querydsl.core.types.Expression;
27+
import com.querydsl.core.types.dsl.Expressions;
28+
import com.querydsl.core.types.dsl.NumberPath;
2729
import com.querydsl.jpa.domain.Cat;
2830
import com.querydsl.jpa.domain.QCat;
2931
import com.querydsl.jpa.domain.QGroup;
@@ -247,4 +249,97 @@ public void subQueryWithOffsetOnly() {
247249
assertThat(results).hasSize(expectedIds.size());
248250
assertThat(results.stream().map(Cat::getId).toList()).isEqualTo(expectedIds);
249251
}
252+
253+
@Test
254+
public void cteWithInnerJoinAndHint() {
255+
// does not work before Hibernate 6.5,
256+
// see https://hibernate.atlassian.net/browse/HHH-17897
257+
258+
// find the heaviest cat that is lighter than Felix
259+
var felix = new QCat("felix");
260+
Cat result =
261+
query()
262+
.withNotMaterializedHint(
263+
felix,
264+
JPAExpressions.select(QCat.cat.bodyWeight.as(felix.bodyWeight))
265+
.from(QCat.cat)
266+
.where(QCat.cat.name.eq("Felix123")))
267+
.select(QCat.cat)
268+
.from(felix)
269+
.join(QCat.cat)
270+
.on(QCat.cat.bodyWeight.lt(felix.bodyWeight))
271+
.orderBy(QCat.cat.bodyWeight.desc())
272+
.limit(1)
273+
.fetchOne();
274+
assertThat(result)
275+
.hasFieldOrPropertyWithValue("id", 2)
276+
.hasFieldOrPropertyWithValue("name", "Ruth123");
277+
}
278+
279+
@Test
280+
public void cteWithCrossJoinAndCustomColumn() {
281+
// all cats in ascending order by comparing their weight to the most average weight of all cats
282+
var avgCat = new QCat("avgcat");
283+
NumberPath<Double> avgWeightColumn = Expressions.numberPath(Double.class, avgCat, "avgweight");
284+
List<Cat> results =
285+
query()
286+
.with(
287+
avgCat,
288+
JPAExpressions.select(QCat.cat.bodyWeight.avg().as(avgWeightColumn)).from(QCat.cat))
289+
.select(QCat.cat)
290+
.from(QCat.cat, avgCat)
291+
.orderBy(QCat.cat.bodyWeight.subtract(avgWeightColumn).abs().asc(), QCat.cat.id.asc())
292+
.fetch();
293+
// the average body weights of all cats is 3.5
294+
assertThat(results)
295+
.hasSize(6)
296+
.satisfiesExactly(
297+
cat ->
298+
assertThat(cat)
299+
.hasFieldOrPropertyWithValue("id", 3)
300+
.hasFieldOrPropertyWithValue("bodyWeight", 3.0D),
301+
cat ->
302+
assertThat(cat)
303+
.hasFieldOrPropertyWithValue("id", 4)
304+
.hasFieldOrPropertyWithValue("bodyWeight", 4.0D),
305+
cat ->
306+
assertThat(cat)
307+
.hasFieldOrPropertyWithValue("id", 2)
308+
.hasFieldOrPropertyWithValue("bodyWeight", 2.0D),
309+
cat ->
310+
assertThat(cat)
311+
.hasFieldOrPropertyWithValue("id", 5)
312+
.hasFieldOrPropertyWithValue("bodyWeight", 5.0D),
313+
cat ->
314+
assertThat(cat)
315+
.hasFieldOrPropertyWithValue("id", 1)
316+
.hasFieldOrPropertyWithValue("bodyWeight", 1.0D),
317+
cat ->
318+
assertThat(cat)
319+
.hasFieldOrPropertyWithValue("id", 6)
320+
.hasFieldOrPropertyWithValue("bodyWeight", 6.0D));
321+
}
322+
323+
@Test
324+
public void multipleCtes() {
325+
QCat felix = new QCat("felix");
326+
QCat felixMates = new QCat("felixMates");
327+
List<Integer> results =
328+
query()
329+
.with(
330+
felix,
331+
JPAExpressions.select(QCat.cat.id.as(felix.id))
332+
.from(QCat.cat)
333+
.where(QCat.cat.name.eq("Felix123")))
334+
.with(
335+
felixMates,
336+
JPAExpressions.select(QCat.cat.id.as(QCat.cat.id))
337+
.from(QCat.cat)
338+
.innerJoin(felix)
339+
.on(QCat.cat.mate.id.eq(felix.id)))
340+
.select(felixMates.id)
341+
.from(felixMates)
342+
.fetch();
343+
assertThat(results).hasSize(1).isEqualTo(List.of(4));
344+
}
250345
}

querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateQueryTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,63 @@ public void innerJoin() {
4343
assertThat(hqlQuery.toString())
4444
.isEqualTo("select employee\nfrom Employee employee\n inner join employee.user as user");
4545
}
46+
47+
@Test
48+
public void cteWithNotMaterializedHint() {
49+
HibernateQuery<Void> query = new HibernateQuery<>();
50+
QCat felix = new QCat("felix");
51+
query
52+
.withNotMaterializedHint(
53+
felix,
54+
JPAExpressions.select(QCat.cat.bodyWeight.as(felix.bodyWeight))
55+
.from(QCat.cat)
56+
.where(QCat.cat.name.eq("Felix123")))
57+
.select(QCat.cat)
58+
.from(QCat.cat, felix)
59+
.where(QCat.cat.bodyWeight.gt(felix.bodyWeight));
60+
assertThat(query.toString())
61+
.isEqualTo(
62+
"""
63+
with
64+
felix as not materialized (select cat.bodyWeight as bodyWeight
65+
from Cat cat
66+
where cat.name = ?1)
67+
select cat
68+
from Cat cat, felix felix
69+
where cat.bodyWeight > felix.bodyWeight""");
70+
}
71+
72+
@Test
73+
public void multipleCtes() {
74+
HibernateQuery<Void> query = new HibernateQuery<>();
75+
QCat felix = new QCat("felix");
76+
QCat felixMates = new QCat("felixMates");
77+
78+
query
79+
.with(
80+
felix,
81+
JPAExpressions.select(QCat.cat.id.as(felix.id))
82+
.from(QCat.cat)
83+
.where(QCat.cat.name.eq("Felix123")))
84+
.with(
85+
felixMates,
86+
JPAExpressions.select(QCat.cat.id.as(felixMates.id))
87+
.from(QCat.cat)
88+
.innerJoin(felix)
89+
.on(QCat.cat.mate.id.eq(felix.id)))
90+
.select(felixMates.id)
91+
.from(felixMates);
92+
assertThat(query.toString())
93+
.isEqualTo(
94+
"""
95+
with
96+
felix as (select cat.id as id
97+
from Cat cat
98+
where cat.name = ?1),
99+
felixMates as (select cat.id as id
100+
from Cat cat
101+
inner join felix felix with cat.mate.id = felix.id)
102+
select felixMates.id
103+
from felixMates felixMates""");
104+
}
46105
}

0 commit comments

Comments
 (0)