Skip to content

Commit c219fd9

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

File tree

6 files changed

+284
-4
lines changed

6 files changed

+284
-4
lines changed

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+
}

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)