Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ private String getEntityName(Class<?> clazz) {
}
}

private void handleJoinTarget(JoinExpression je) {
protected void handleJoinTarget(JoinExpression je) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find a nicer way to extend the logic of this method.
As NativeSQLSerializer also has a similarly named method, which is also marked as protected I hope this is okay to do.

// type specifier
if (je.getTarget() instanceof EntityPath<?>) {
final EntityPath<?> pe = (EntityPath<?>) je.getTarget();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
import com.querydsl.core.DefaultQueryMetadata;
import com.querydsl.core.NonUniqueResultException;
import com.querydsl.core.QueryException;
import com.querydsl.core.QueryFlag;
import com.querydsl.core.QueryMetadata;
import com.querydsl.core.QueryModifiers;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.ConstantImpl;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.FactoryExpression;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.SubQueryExpression;
import com.querydsl.jpa.FactoryExpressionTransformer;
import com.querydsl.jpa.HQLTemplates;
import com.querydsl.jpa.JPAQueryBase;
import com.querydsl.jpa.JPQLSerializer;
import com.querydsl.jpa.JPQLTemplates;
import com.querydsl.jpa.ScrollableResultsIterator;
import java.util.HashMap;
Expand Down Expand Up @@ -95,6 +98,29 @@ public long fetchCount() {
}
}

public Q with(Path<?> alias, SubQueryExpression<?> query) {
return with(alias, null, query);
}
Comment on lines +101 to +103
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hibernate only supports subqueries here, so there is no WithBuilder options here.
Also, recursive CTEs are not in scope for this change, so those are also not present.


public Q withMaterializedHint(Path<?> alias, SubQueryExpression<?> query) {
return with(alias, true, query);
}

public Q withNotMaterializedHint(Path<?> alias, SubQueryExpression<?> query) {
return with(alias, false, query);
}

public Q with(Path<?> alias, Boolean materialized, SubQueryExpression<?> query) {
Expression<?> expr =
ExpressionUtils.operation(
alias.getType(),
HQLOps.WITH,
alias,
materialized != null ? ConstantImpl.create(materialized) : null,
query);
return queryMixin.addFlag(new QueryFlag(QueryFlag.Position.WITH, expr));
}

/**
* Expose the original Hibernate query for the given projection
*
Expand Down Expand Up @@ -348,8 +374,8 @@ public T fetchOne() throws NonUniqueResultException {
}

@Override
protected JPQLSerializer createSerializer() {
return new JPQLSerializer(getTemplates());
protected HQLSerializer createSerializer() {
return new HQLSerializer(getTemplates());
}

protected void clone(Q query) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.querydsl.jpa.hibernate;

import com.querydsl.core.types.Operator;

public enum HQLOps implements Operator {
WITH(Object.class);

private final Class<?> type;

HQLOps(Class<?> type) {
this.type = type;
}

@Override
public Class<?> getType() {
return type;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.querydsl.jpa.hibernate;

import com.querydsl.core.JoinExpression;
import com.querydsl.core.QueryFlag;
import com.querydsl.core.QueryMetadata;
import com.querydsl.core.types.ConstantImpl;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Operator;
import com.querydsl.core.types.Path;
import com.querydsl.jpa.JPQLSerializer;
import com.querydsl.jpa.JPQLTemplates;
import jakarta.persistence.EntityManager;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;

public class HQLSerializer extends JPQLSerializer {

protected final Set<Path<?>> withAliases = new HashSet<>();

public HQLSerializer(JPQLTemplates templates) {
super(templates);
}

public HQLSerializer(JPQLTemplates templates, EntityManager em) {
super(templates, em);
}

@Override
public void serialize(QueryMetadata metadata, boolean forCountRow, @Nullable String projection) {
final Set<QueryFlag> flags = metadata.getFlags();
final var hasFlags = !flags.isEmpty();

if (hasFlags) {
List<Expression<?>> withFlags = new ArrayList<>();
for (QueryFlag flag : flags) {
if (flag.getPosition() == QueryFlag.Position.WITH) {
withFlags.add(flag.getFlag());
}
}
if (!withFlags.isEmpty()) {
append("with\n");
handle(",\n", withFlags);
append("\n");
}
}

super.serialize(metadata, forCountRow, projection);
}

@Override
protected void visitOperation(
Class<?> type, Operator operator, List<? extends Expression<?>> args) {
if (operator == HQLOps.WITH && args.size() == 3 && args.get(0) instanceof Path<?> alias) {
handle(alias);
withAliases.add(alias);
append(" as ");
if (args.get(1) instanceof ConstantImpl<?> materializedParam
&& materializedParam.getConstant() instanceof Boolean materialized) {
if (!materialized) {
append("not ");
}
append("materialized ");
}
handle(args.get(2));
Comment on lines +57 to +67
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I wanted to use templates, but I couldn't figure out how to get the (not)? materialized part work with them. So I just append directly here.

} else {
super.visitOperation(type, operator, args);
}
}

@Override
protected void handleJoinTarget(JoinExpression je) {
if (je.getTarget() instanceof Path<?> pe && withAliases.contains(pe)) {
append(pe.getMetadata().getName()).append(" ");
handle(je.getTarget());
} else {
super.handleJoinTarget(je);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.querydsl.core.testutil.ExcludeIn;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.jpa.domain.Cat;
import com.querydsl.jpa.domain.QCat;
import com.querydsl.jpa.domain.QGroup;
Expand Down Expand Up @@ -247,4 +249,97 @@ public void subQueryWithOffsetOnly() {
assertThat(results).hasSize(expectedIds.size());
assertThat(results.stream().map(Cat::getId).toList()).isEqualTo(expectedIds);
}

@Test
public void cteWithInnerJoinAndHint() {
// does not work before Hibernate 6.5,
// see https://hibernate.atlassian.net/browse/HHH-17897

// find the heaviest cat that is lighter than Felix
var felix = new QCat("felix");
Cat result =
query()
.withNotMaterializedHint(
felix,
JPAExpressions.select(QCat.cat.bodyWeight.as(felix.bodyWeight))
.from(QCat.cat)
.where(QCat.cat.name.eq("Felix123")))
.select(QCat.cat)
.from(felix)
.join(QCat.cat)
.on(QCat.cat.bodyWeight.lt(felix.bodyWeight))
.orderBy(QCat.cat.bodyWeight.desc())
.limit(1)
.fetchOne();
assertThat(result)
.hasFieldOrPropertyWithValue("id", 2)
.hasFieldOrPropertyWithValue("name", "Ruth123");
}

@Test
public void cteWithCrossJoinAndCustomColumn() {
// all cats in ascending order by comparing their weight to the most average weight of all cats
var avgCat = new QCat("avgcat");
NumberPath<Double> avgWeightColumn = Expressions.numberPath(Double.class, avgCat, "avgweight");
List<Cat> results =
query()
.with(
avgCat,
JPAExpressions.select(QCat.cat.bodyWeight.avg().as(avgWeightColumn)).from(QCat.cat))
.select(QCat.cat)
.from(QCat.cat, avgCat)
.orderBy(QCat.cat.bodyWeight.subtract(avgWeightColumn).abs().asc(), QCat.cat.id.asc())
.fetch();
// the average body weights of all cats is 3.5
assertThat(results)
.hasSize(6)
.satisfiesExactly(
cat ->
assertThat(cat)
.hasFieldOrPropertyWithValue("id", 3)
.hasFieldOrPropertyWithValue("bodyWeight", 3.0D),
cat ->
assertThat(cat)
.hasFieldOrPropertyWithValue("id", 4)
.hasFieldOrPropertyWithValue("bodyWeight", 4.0D),
cat ->
assertThat(cat)
.hasFieldOrPropertyWithValue("id", 2)
.hasFieldOrPropertyWithValue("bodyWeight", 2.0D),
cat ->
assertThat(cat)
.hasFieldOrPropertyWithValue("id", 5)
.hasFieldOrPropertyWithValue("bodyWeight", 5.0D),
cat ->
assertThat(cat)
.hasFieldOrPropertyWithValue("id", 1)
.hasFieldOrPropertyWithValue("bodyWeight", 1.0D),
cat ->
assertThat(cat)
.hasFieldOrPropertyWithValue("id", 6)
.hasFieldOrPropertyWithValue("bodyWeight", 6.0D));
}

@Test
public void multipleCtes() {
QCat felix = new QCat("felix");
QCat felixMates = new QCat("felixMates");
List<Integer> results =
query()
.with(
felix,
JPAExpressions.select(QCat.cat.id.as(felix.id))
.from(QCat.cat)
.where(QCat.cat.name.eq("Felix123")))
.with(
felixMates,
JPAExpressions.select(QCat.cat.id.as(QCat.cat.id))
.from(QCat.cat)
.innerJoin(felix)
.on(QCat.cat.mate.id.eq(felix.id)))
.select(felixMates.id)
.from(felixMates)
.fetch();
assertThat(results).hasSize(1).isEqualTo(List.of(4));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,63 @@ public void innerJoin() {
assertThat(hqlQuery.toString())
.isEqualTo("select employee\nfrom Employee employee\n inner join employee.user as user");
}

@Test
public void cteWithNotMaterializedHint() {
HibernateQuery<Void> query = new HibernateQuery<>();
QCat felix = new QCat("felix");
query
.withNotMaterializedHint(
felix,
JPAExpressions.select(QCat.cat.bodyWeight.as(felix.bodyWeight))
.from(QCat.cat)
.where(QCat.cat.name.eq("Felix123")))
.select(QCat.cat)
.from(QCat.cat, felix)
.where(QCat.cat.bodyWeight.gt(felix.bodyWeight));
assertThat(query.toString())
.isEqualTo(
"""
with
felix as not materialized (select cat.bodyWeight as bodyWeight
from Cat cat
where cat.name = ?1)
select cat
from Cat cat, felix felix
where cat.bodyWeight > felix.bodyWeight""");
}

@Test
public void multipleCtes() {
HibernateQuery<Void> query = new HibernateQuery<>();
QCat felix = new QCat("felix");
QCat felixMates = new QCat("felixMates");

query
.with(
felix,
JPAExpressions.select(QCat.cat.id.as(felix.id))
.from(QCat.cat)
.where(QCat.cat.name.eq("Felix123")))
.with(
felixMates,
JPAExpressions.select(QCat.cat.id.as(felixMates.id))
.from(QCat.cat)
.innerJoin(felix)
.on(QCat.cat.mate.id.eq(felix.id)))
.select(felixMates.id)
.from(felixMates);
assertThat(query.toString())
.isEqualTo(
"""
with
felix as (select cat.id as id
from Cat cat
where cat.name = ?1),
felixMates as (select cat.id as id
from Cat cat
inner join felix felix with cat.mate.id = felix.id)
select felixMates.id
from felixMates felixMates""");
}
}
Loading