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 javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

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.