HQL and JPQL
|
This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Three older reference books were consulted as bibliography only while preparing these pages and are not the
primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the
This section’s bibliography lists the reference material consulted while preparing these pages. |
HQL (Hibernate Query Language) is Hibernate’s own object-oriented query language; JPQL (the JPA-standard
query language) is a strict subset of it — any valid JPQL query is valid HQL, but HQL adds syntax JPQL does
not define. This page covers HQL/JPQL syntax and the Query API in depth; for how Spring Data wires @Query
strings into repository methods, see Spring Data JPA — this page
stays one layer below that, on the language itself.
Statement types
HQL supports SELECT, UPDATE, DELETE, and INSERT … SELECT statements, all operating over entity names
and their fields rather than table and column names:
// SELECT
em.createQuery("SELECT b FROM Book b WHERE b.title LIKE :pattern", Book.class);
// bulk UPDATE / DELETE -- see bulk-operations-and-batching.adoc for why these bypass the persistence context
em.createQuery("UPDATE Book b SET b.outOfPrint = true WHERE b.publishedYear < :year")
.setParameter("year", 1990)
.executeUpdate();
FROM/JOIN: explicit, implicit, WITH/ON
// explicit join
"SELECT b FROM Book b JOIN b.author a WHERE a.name = :name"
// join fetch -- see fetching-and-n-plus-1.adoc
"SELECT b FROM Book b JOIN FETCH b.author"
// implicit join via path expression (equivalent inner join, no alias for author)
"SELECT b FROM Book b WHERE b.author.name = :name"
// WITH / ON -- extra join-time predicate beyond the association's own mapping
"SELECT a FROM Author a LEFT JOIN a.books b WITH b.publishedYear > :year"
WITH (HQL) and JPQL’s standardized ON are equivalent for this purpose in modern Hibernate — both add a
join-time condition beyond the association’s own foreign key, most useful for a LEFT JOIN where filtering in
the main WHERE clause instead would incorrectly turn it into an inner join.
Functions, predicates, and projections
Standard JPQL functions (LOWER, UPPER, LENGTH, CONCAT, SUBSTRING, TRIM, arithmetic, COALESCE,
NULLIF, CASE WHEN) plus HQL extensions (FORMAT, date/time component extraction, CAST); predicates
include BETWEEN, IN, LIKE, IS NULL, EXISTS, and JPQL’s MEMBER OF for collection membership.
SELECT new projects query results directly into a DTO constructor instead of full entities — the recommended
shape for read-only views (see Fetching & N+1):
public record BookSummary(Long id, String title, String authorName) {}
List<BookSummary> summaries = em.createQuery(
"SELECT new com.example.BookSummary(b.id, b.title, a.name) FROM Book b JOIN b.author a",
BookSummary.class).getResultList();
Set operations: UNION/INTERSECT/EXCEPT
HQL supports combining the results of several SELECT queries with the same JPQL-level set operators SQL
offers, translated to the target database’s own syntax (or emulated where unsupported):
em.createQuery("""
SELECT b.title FROM Book b WHERE b.publishedYear > 2020
UNION
SELECT b.title FROM Book b WHERE b.author.name = :name
""", String.class);
Aggregation and grouping
COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING — standard SQL-style aggregation, over entity paths:
em.createQuery("""
SELECT a.name, COUNT(b), AVG(b.price)
FROM Book b JOIN b.author a
GROUP BY a.name
HAVING COUNT(b) > 5
""", Object[].class);
The Query API, parameters, and pagination
TypedQuery<Book> query = em.createQuery("FROM Book b WHERE b.publishedYear = :year", Book.class);
query.setParameter("year", 2024); // named parameter
query.setFirstResult(20); // offset
query.setMaxResults(10); // page size
List<Book> page = query.getResultList();
// Hibernate's own Limit/Page API (alternative to setFirstResult/setMaxResults)
Query<Book> hquery = session.createQuery("FROM Book", Book.class);
hquery.setPage(Page.first(10));
Offset-based pagination (setFirstResult/setMaxResults) gets slower as the offset grows, since the database
still has to scan and discard every skipped row. Key-based (seek) pagination avoids this by filtering on the
last-seen sort key instead of an offset:
// page N+1: everything after the last id seen on the previous page, same order
"FROM Book b WHERE b.id > :lastSeenId ORDER BY b.id ASC"
@NamedQuery
@Entity
@NamedQuery(
name = "Book.findByAuthorName",
query = "SELECT b FROM Book b JOIN b.author a WHERE a.name = :name")
public class Book { /* ... */ }
// ...
em.createNamedQuery("Book.findByAuthorName", Book.class).setParameter("name", "King").getResultList();
Named queries are validated and their query plan is prepared at EntityManagerFactory bootstrap rather than
the first time they run, catching a typo at startup instead of at request time.
Scrolling and streaming
For result sets too large to materialize as a List at once:
// Hibernate native scrolling -- a server-side cursor
try (ScrollableResults<Book> results = session.createQuery("FROM Book", Book.class)
.scroll(ScrollMode.FORWARD_ONLY)) {
while (results.next()) {
Book book = results.get();
// process one at a time; periodically clear() the session, see bulk-operations-and-batching.adoc
}
}
// JPA streaming
try (Stream<Book> stream = em.createQuery("FROM Book", Book.class).getResultStream()) {
stream.forEach(this::process);
}
Both still hold one JDBC ResultSet open for the duration; they bound heap usage (no giant List materialized
at once), not the database-side or persistence-context cost of processing many rows — pair with periodic
session.clear() for very large scans.
hibernate.query.* settings
hibernate.query.plan_cache_max_size bounds how many parsed-and-translated query plans Hibernate keeps cached
(a cache miss re-parses the JPQL/HQL string, so highly dynamic query strings can thrash this cache — prefer
parameters over string-concatenated literals for exactly this reason, in addition to avoiding SQL injection).
hibernate.query.fail_on_pagination_over_collection_fetch catches, at query-plan time, the specific mistake of
combining setFirstResult/setMaxResults with a JOIN FETCH of a to-many collection — pagination there
operates on the joined (denormalized) row count, not the number of distinct parent entities, silently returning
a wrong page.