Skip to content
Merged
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
97 changes: 97 additions & 0 deletions docs/tutorials/hibernate.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,102 @@ List results = hibernateQuery.list();
This returns a Hibernate `org.hibernate.query.Query` rather than a JPA
`jakarta.persistence.Query`.

## Common Table Expressions

`HibernateQuery` supports Common Table Expressions (CTEs) via the `with()`
method family. CTEs define named temporary result sets that can be referenced
in the main query. This feature requires Hibernate 6.5 or later and is not
available on `JPAQuery`.

| Method | Description |
|---|---|
| `with(alias, subquery)` | Define a CTE |
| `withMaterializedHint(alias, subquery)` | Define a CTE with `MATERIALIZED` hint |
| `withNotMaterializedHint(alias, subquery)` | Define a CTE with `NOT MATERIALIZED` hint |

### Simple CTE with Join

Define a CTE that selects a specific cat's weight, then join against it to
find lighter cats:

```java
QCat cat = QCat.cat;
QCat felix = new QCat("felix");

Cat result = new HibernateQuery<Cat>(session)
.withNotMaterializedHint(felix,
JPAExpressions.select(cat.bodyWeight.as(felix.bodyWeight))
.from(cat)
.where(cat.name.eq("Felix")))
.select(cat)
.from(felix)
.join(cat).on(cat.bodyWeight.lt(felix.bodyWeight))
.orderBy(cat.bodyWeight.desc())
.limit(1)
.fetchOne();
```

Generated HQL:

```
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
```

### CTE with Custom Column

Use `Expressions.numberPath()` to create custom column references within a CTE:

```java
QCat cat = QCat.cat;
QCat avgCat = new QCat("avgcat");
NumberPath<Double> avgWeight = Expressions.numberPath(Double.class, avgCat, "avgweight");

List<Cat> results = new HibernateQuery<Cat>(session)
.with(avgCat,
JPAExpressions.select(cat.bodyWeight.avg().as(avgWeight))
.from(cat))
.select(cat)
.from(cat, avgCat)
.orderBy(cat.bodyWeight.subtract(avgWeight).abs().asc(), cat.id.asc())
.fetch();
```

### Multiple CTEs

Chain `.with()` calls to define multiple CTEs. Later CTEs can reference earlier
ones:

```java
QCat cat = QCat.cat;
QCat felix = new QCat("felix");
QCat felixMates = new QCat("felixMates");

List<Integer> results = new HibernateQuery<Integer>(session)
.with(felix,
JPAExpressions.select(cat.id.as(felix.id))
.from(cat)
.where(cat.name.eq("Felix")))
.with(felixMates,
JPAExpressions.select(cat.id.as(cat.id))
.from(cat)
.innerJoin(felix).on(cat.mate.id.eq(felix.id)))
.select(felixMates.id)
.from(felixMates)
.fetch();
```

{: .note }
CTE aliases reuse existing Q-types (e.g., `new QCat("felix")`) to define CTE
columns. Use `as()` in projections to map columns from the subquery to the CTE
alias fields. The `JPAExpressions` factory is used for CTE subqueries, same as
for standard JPQL subqueries.

## Native SQL with Hibernate

Use `HibernateSQLQuery` to run native SQL through a Hibernate `Session`:
Expand All @@ -173,6 +269,7 @@ for more details on native SQL query patterns.
|---|---|---|
| Underlying API | JPA EntityManager | Hibernate Session |
| Query factory | `JPAQueryFactory` | `HibernateQueryFactory` |
| Common Table Expressions | Not available | `with()`, `withMaterializedHint()`, `withNotMaterializedHint()` |
| Query caching | Not available | `setCacheable()`, `setCacheRegion()` |
| Read-only mode | Not available | `setReadOnly()` |
| SQL comments | Not available | `setComment()` |
Expand Down
8 changes: 5 additions & 3 deletions docs/tutorials/jpa.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ The `JPAAnnotationProcessor` finds domain types annotated with the
If you use Hibernate annotations in your domain types, use the processor
`com.querydsl.apt.hibernate.HibernateAnnotationProcessor` instead. See the
[Hibernate tutorial]({{ site.baseurl }}/tutorials/hibernate) for
Hibernate-specific features such as query caching and read-only mode.
Hibernate-specific features such as Common Table Expressions, query caching,
and read-only mode.

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

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

To retrieve the customer with the first name Bob:
Expand Down
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
Copy Markdown
Contributor 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
Copy Markdown
Contributor 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
Copy Markdown
Contributor 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);
}
}
}
Loading
Loading