JPQL, Criteria API & Queries

Parameters & Named Queries

18 min Lesson 4 of 13

Parameters & Named Queries

Every practical JPQL query needs external values — a user ID to filter by, a status string to match, a date range to search within. Embedding those values directly as string literals is both insecure (SQL injection via JPQL) and inefficient (the persistence provider cannot reuse a compiled query plan). JPQL therefore provides two parameter-binding styles, and JPA complements them with the @NamedQuery mechanism that pre-declares queries at class load time.

Positional Parameters

Positional parameters are placeholders written as ?1, ?2, … (one-based). You bind values via TypedQuery.setParameter(int, Object).

import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import java.util.List; public class OrderRepository { private final EntityManager em; public OrderRepository(EntityManager em) { this.em = em; } public List<Order> findByCustomerAndStatus(Long customerId, String status) { TypedQuery<Order> q = em.createQuery( "SELECT o FROM Order o WHERE o.customer.id = ?1 AND o.status = ?2", Order.class ); q.setParameter(1, customerId); q.setParameter(2, status); return q.getResultList(); } }

Positional parameters are concise but fragile — if you reorder the WHERE clause you must also renumber every setParameter call. For anything beyond a one-liner, named parameters are almost always preferable.

Named Parameters

Named parameters use the colon prefix :name and are bound via setParameter(String, Object). The name is self-documenting and position-independent.

public List<Order> findByCustomerAndStatus(Long customerId, String status) { return em.createQuery( "SELECT o FROM Order o " + "WHERE o.customer.id = :customerId AND o.status = :status", Order.class) .setParameter("customerId", customerId) .setParameter("status", status) .getResultList(); }
Always use named parameters in production code. They make the intent clear at the call site, survive clause reordering without breaking, and are mandatory when using @NamedQuery.

Temporal and Special Types

When binding java.util.Date (legacy API) you had to supply a TemporalType hint. With the modern java.time API — LocalDate, LocalDateTime, Instant — Hibernate 6 maps them natively, so no hint is needed.

import java.time.LocalDate; public List<Order> findOrdersAfter(LocalDate since) { return em.createQuery( "SELECT o FROM Order o WHERE o.placedAt >= :since", Order.class) .setParameter("since", since) // no TemporalType needed with java.time .getResultList(); }
Null parameters are safe. Passing null to setParameter emits IS NULL in the generated SQL — you do not need a separate query branch for optional filters when you use the Criteria API, but with JPQL string queries you usually write explicit null-checks in the JPQL or use separate query methods.

Pagination: setFirstResult and setMaxResults

Pagination is controlled via TypedQuery fluently, not through SQL-dialect-specific clauses like LIMIT/OFFSET. This keeps the JPQL portable across databases.

public List<Order> findPage(String status, int page, int pageSize) { return em.createQuery( "SELECT o FROM Order o WHERE o.status = :status ORDER BY o.placedAt DESC", Order.class) .setParameter("status", status) .setFirstResult(page * pageSize) // zero-based offset .setMaxResults(pageSize) .getResultList(); }

Declaring Named Queries with @NamedQuery

A named query is a JPQL string that is declared once — on the entity class — and referenced by a logical name everywhere else. JPA parses and validates the JPQL at application start-up (when the EntityManagerFactory is built), not at runtime. This means syntax errors are caught on boot, not on the first user request.

import jakarta.persistence.*; @Entity @Table(name = "orders") @NamedQuery( name = "Order.findByStatus", query = "SELECT o FROM Order o WHERE o.status = :status ORDER BY o.placedAt DESC" ) @NamedQuery( name = "Order.findByCustomer", query = "SELECT o FROM Order o WHERE o.customer.id = :customerId" ) public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String status; @Column(name = "placed_at") private java.time.LocalDateTime placedAt; @ManyToOne(fetch = FetchType.LAZY) private Customer customer; // getters / setters omitted }

Multiple named queries on the same entity use @NamedQueries({ @NamedQuery(...), @NamedQuery(...) }), or in Java 8+ you can simply repeat the annotation as shown above (repeatable annotation support).

Executing a Named Query

Call em.createNamedQuery(name, resultClass) instead of em.createQuery(jpqlString, resultClass). Parameter binding is identical.

public List<Order> findByStatus(String status) { return em.createNamedQuery("Order.findByStatus", Order.class) .setParameter("status", status) .getResultList(); }
Convention: name your named queries EntityName.methodName (e.g., Order.findByStatus). Spring Data JPA looks for a named query by this convention automatically before generating a query from the method name, so the naming is doubly useful.

Named Queries in Spring Data JPA Repositories

If you use Spring Data JPA, a repository method named findByStatus on an Order repository automatically resolves the named query Order.findByStatus if it exists. You can also annotate the repository method with @Query for an inline alternative, but pre-declared named queries have the advantage of boot-time validation.

import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface OrderRepository extends JpaRepository<Order, Long> { // Spring Data finds @NamedQuery("Order.findByStatus") automatically List<Order> findByStatus(String status); // Alternatively, inline with @Query — validated at runtime, not boot time // @Query("SELECT o FROM Order o WHERE o.customer.id = :id") // List<Order> findByCustomerId(@Param("id") Long id); }

Performance Considerations

  • Query plan caching: When you use parameter binding (both styles), the persistence provider compiles the JPQL once and caches the plan. Concatenating values directly into the JPQL string forces recompilation on every call and forfeits this cache entirely.
  • Named queries are compiled once at startup: Their plan is reused on every call with no re-parsing overhead, making them the most efficient JPQL execution path.
  • Use getSingleResult carefully: It throws NoResultException if no row matches and NonUniqueResultException for multiple rows. Wrap it or use getResultList() and test isEmpty() if zero results are possible.
Never build JPQL strings by string concatenation with user input. JPQL injection is a real attack vector — an attacker can escape your intended predicate and append arbitrary JPQL. Always use positional or named parameters to safely pass external values.

Summary

Use named parameters (:name) in preference to positional parameters for readability and resilience. Leverage setFirstResult/setMaxResults for database-portable pagination. Declare frequently executed or shared queries as @NamedQuery on the entity class to get boot-time syntax validation and the most efficient query-plan reuse. In the next lesson you will see these techniques combined with aggregate functions, grouping, and HAVING clauses.