@Query & JPQL Queries
@Query & JPQL Queries
Derived query methods are convenient, but they have limits. Once a query requires a JOIN across two tables, an aggregate, a subquery, or any logic that cannot be expressed as a clean method name, you need to write the query yourself. Spring Data JPA gives you a single annotation for that purpose: @Query.
What Is JPQL?
JPQL (Jakarta Persistence Query Language) is the query language defined by the JPA specification. It looks almost identical to SQL, but it operates on entities and their fields, not on tables and columns. Hibernate (the JPA provider inside Spring Boot) translates your JPQL into the correct SQL dialect at runtime — PostgreSQL, MySQL, H2, or whatever you configured.
Consider a simple entity:
In JPQL you reference Order (the class name) and o.status (the field), never the table name orders or the column status. This indirection is what makes your code independent of the physical schema.
Basic @Query Usage
Place the annotation directly above the repository method. The value attribute holds the JPQL string. Bind parameters with the :name syntax and match them to method parameters using @Param:
Order), not the database table name (orders). If you rename the table but keep the class name the same, your JPQL does not change.
Positional vs Named Parameters
JPQL supports both styles. Named parameters (:name + @Param) are strongly preferred because they survive parameter reordering during refactoring:
Joining Across Associations
Because JPQL understands the object model, you can traverse associations with a dot or an explicit JOIN. For performance-critical code, an explicit JOIN FETCH is the right tool — it tells Hibernate to load the association in a single SQL query instead of issuing one query per entity (the N+1 problem):
JOIN FETCH with Pageable, Hibernate must load all matching rows into memory before paginating, which defeats the purpose. Use a two-query strategy instead: one @Query with JOIN FETCH and a separate countQuery attribute on the @Query, or use a DTO projection (covered in the next lesson).
Returning Non-Entity Results
You are not limited to returning entity objects. @Query can project onto a subset of fields using a constructor expression or an interface projection:
Modifying Queries: UPDATE and DELETE
By default @Query is read-only. To run a DML statement (UPDATE or DELETE in JPQL, or any SQL mutation) you must add two annotations:
@Modifying— tells Spring Data this query mutates state.@Transactional— every write must run inside a transaction.
The return type int (or Integer) gives you the count of affected rows. The method can also return void.
Order entity, then run a bulk UPDATE in the same transaction, the entity in memory still holds the old value — Hibernate does not refresh it automatically. Either call entityManager.clear() after the bulk update, or set @Modifying(clearAutomatically = true) to let Spring Data do it for you.
The countQuery Attribute
When your @Query uses a JOIN FETCH or a complex SELECT that Hibernate cannot automatically turn into a COUNT query for pagination, provide an explicit count query:
JPQL vs HQL vs Criteria
JPQL is the JPA standard. Hibernate's own dialect (HQL) is a superset — it adds features like TREAT casts and extended arithmetic — but prefer standard JPQL unless you have a concrete reason. The Criteria API (covered in a later lesson of this tutorial series) is the type-safe programmatic alternative, useful when query shape must be determined at runtime.
Summary
@Query with JPQL is the primary tool for any query that cannot be expressed as a derived method name. Use named parameters and @Param for maintainability, use JOIN FETCH to eliminate N+1 problems, use constructor expressions or interface projections when you only need a subset of fields, and add @Modifying + @Transactional for any DML statement. These patterns cover the large majority of real-world repository queries.