Files
hibernate-demo/docs/14-named-queries.md
T

12 KiB
Executable File

14 — Named queries: what startup validation, caching, and "faster" actually mean

← Previous: 13 — Date and time mapping | Back to README → | Next: 15 — HQL queries →

Backs ankurm.com: Hibernate 7 named queries.

Everything below comes from JUnit tests in src/test/java/com/ankurm/hibernatedemo/namedquery/ (and one deliberately-broken entity, BrokenNamedQueryEmployee, kept outside the app's scanned package, explained below), run against Hibernate 7.4.5.Final / H2 2.4.240.

Startup validation

hibernate.query.startup_check is a real setting -- confirmed present as org.hibernate.cfg.QuerySettings.QUERY_STARTUP_CHECKING -- and it does exactly what the name suggests.

A deliberately broken @NamedQuery (e.firsNam instead of e.firstName) fails SessionFactory construction with the check enabled (Hibernate's default):

wrapper class: org.hibernate.query.NamedQueryValidationException
verbatim message: Errors in named queries:
  [1] Error in query named 'BrokenNamedQueryEmployee.badProperty': Could not resolve attribute
      'firsNam' of 'com.ankurm.brokenprobe.BrokenNamedQueryEmployee'
      [SELECT e FROM BrokenNamedQueryEmployee e WHERE e.firsNam = :name]

With hibernate.query.startup_check=false, the identical broken entity builds a SessionFactory successfully. The same broken query only fails once it is actually executed -- and with a different exception:

class: java.lang.IllegalArgumentException
message: org.hibernate.query.sqm.UnknownPathException: Could not resolve attribute 'firsNam' ...

NamedQueryValidationException at boot vs. IllegalArgumentException (wrapping UnknownPathException) at call time -- the contrast is the article's best argument for leaving the check on: the failure mode changes from "the deploy pipeline stops" to "a user's request throws in production."

Engineering note on how this was reproduced safely: the broken entity (com.ankurm.brokenprobe.BrokenNamedQueryEmployee) lives outside the com.ankurm.hibernatedemo package tree on purpose. Spring Boot's default JPA entity scan walks every subpackage under the @SpringBootApplication class's package (com.ankurm.hibernatedemo), so a broken @NamedQuery anywhere in that tree would fail @SpringBootTest context bootstrap for every test in this shared repository, not just this one. NamedQueryStartupValidationTest instead uses a fully standalone Hibernate bootstrap (StandardServiceRegistryBuilder + MetadataSources, no Spring involved at all) so the broken entity never touches the shared application context.

Raw output: docs/output/namedquery-startup-validation.txt.

jakarta.persistence.NamedQuery vs org.hibernate.annotations.NamedQuery

Both exist and are usable in Hibernate 7.4.5 / Jakarta Persistence 3.2, confirmed via javap. The JPA-standard annotation is minimal:

jakarta.persistence.NamedQuery: name(), query(), resultClass(), lockMode(), hints()

Hibernate's own extends that meaningfully, exercised on HibernateExtraEmployee:

org.hibernate.annotations.NamedQuery: name(), query(), resultClass(), flush(), flushMode(),
  cacheable(), cacheRegion(), fetchSize(), timeout(), comment(),
  cacheStoreMode(), cacheRetrieveMode(), cacheMode(), readOnly()

cacheable, flush/flushMode, timeout, and readOnly have no JPA-standard equivalent on @NamedQuery itself (JPA's hints() array can express some of these indirectly via magic strings, but Hibernate's annotation gives typed attributes).

One of these extras demonstrated actually taking effect: cacheable = true on HibernateExtraEmployee.cacheableFindAll genuinely populates the second-level query cache (JCache

cacheable=true named query: puts after 1st run = 1, cache hits after 2nd run = 1

The put on the first call and the hit on the second are both real, measured, not assumed.

Raw output: docs/output/namedquery-execution-and-projections.txt.

@NamedNativeQuery + @SqlResultSetMapping, and the JPA 3.2 alternative

A @NamedNativeQuery mapped via @SqlResultSetMapping with @ConstructorResult into a plain DTO class (EmployeeDto, on NqEmployee) works exactly as documented, per NamedQueryExecutionTest:

Employee.byNativeDto(ACTIVE): [EmployeeDto{id=1, firstName=Native1}]

Jakarta Persistence 3.2 does allow a record as a JPQL constructor-expression target -- tested directly, not merely inferred from the spec text:

record EmployeeRecordDto(Long id, String firstName) {}

SELECT NEW com.ankurm.hibernatedemo.namedquery.EmployeeRecordDto(e.id, e.firstName)
FROM NqEmployee e WHERE e.firstName = :name
JPQL constructor expression into a record: [EmployeeRecordDto[id=3, firstName=RecordTest]]

No special configuration needed -- a canonical EmployeeRecordDto constructor is matched exactly like any other multi-argument constructor.

Raw output: docs/output/namedquery-execution-and-projections.txt.

Named queries in orm.xml

A named query defined purely in META-INF/orm.xml (no annotation at all, XmlQueryEmployee.findBySalaryAboveXml) is picked up automatically by Spring Boot's default JPA bootstrap -- no persistence.xml and no explicit <mapping-file> registration required; it is discovered simply by being at the conventional META-INF/orm.xml classpath location. This is the same "orm.xml just works with zero registration" theme chapter 04 documents for spring.jpa.mapping-resources-driven entities -- see 04 — Annotations vs. XML mappings.

It works side by side with an annotation-defined named query on the same entity, per OrmXmlNamedQueryTest:

annotation-defined named query result: 1 rows
orm.xml-defined named query result: 1 rows

And when orm.xml defines a named query with the same name as one already declared via @NamedQuery on the entity, the XML definition wins -- proven by giving the annotated version a deliberately wrong predicate (salary < 0) and the XML version the correct one:

XmlQueryEmployee.overridden (annotation says salary<0, orm.xml says salary>:min): 1 rows

If the annotation had won, this would have returned 0 rows.

Raw output: docs/output/namedquery-ormxml.txt.

Does pre-parsing actually help? (Measured, not assumed)

The common claim is that named queries are faster because they are "pre-parsed." Hibernate's own query-plan cache is keyed by the query string, not by whether the string came from a @NamedQuery or an inline JPQL literal -- so after the very first execution of either, both paths hit the same cached AST/plan. NamedQueryPerformanceTest measures this directly: 500 warmup iterations, then 5000 measured iterations of a named query and the identical inline JPQL string, interleaved call-by-call (to cancel out JIT/GC ordering bias) on a shared, otherwise-idle in-memory H2 database:

run 1: named avg=124.18 us/call, inline avg=113.77 us/call, ratio (named/inline)=1.09
run 2: named avg=136.74 us/call, inline avg=128.27 us/call, ratio (named/inline)=1.07

Honest finding: across two runs the ratio stayed within ~10% either direction of 1.0, which is noise for a shared sandbox container, not a real effect. We could not measure a performance advantage for named queries over the identical inline JPQL string once both have been warmed up. The commonly repeated "named queries are faster because they're pre-parsed" claim should be retired as stated -- the real, verifiable benefits of named queries are the ones demonstrated above: fail-fast startup validation, a place to attach Hibernate-specific extras like cacheable, and centralizing query text -- not raw per-call execution speed.

Raw output: docs/output/namedquery-preparse-performance.txt.

getSingleResultOrNull() vs getSingleResult()

Both confirmed present via javap jakarta.persistence.Query (Jakarta Persistence 3.2.0):

public abstract java.lang.Object getSingleResult();
public abstract java.lang.Object getSingleResultOrNull();

Behavior on zero rows, from a real run:

getSingleResultOrNull() on zero rows returned: null
getSingleResult() on zero rows threw: jakarta.persistence.NoResultException:
  No result found for query [SELECT e FROM NqEmployee e WHERE e.firstName = :n]

getSingleResultOrNull() (added in Jakarta Persistence 3.2) is the null-returning alternative that avoids a try/catch around NoResultException for the common "may or may not exist" lookup. Chapter 05 covers the same method on the more specific TypedQuery interface -- see 05 — JPA persistence annotations.

Raw output: docs/output/namedquery-execution-and-projections.txt.

Summary

Claim Verified value
hibernate.query.startup_check Exists (QuerySettings.QUERY_STARTUP_CHECKING); default behavior fails fast at boot
Broken named query, check enabled NamedQueryValidationException at SessionFactory build
Broken named query, check disabled Boots fine; fails at call time with IllegalArgumentException/UnknownPathException
jakarta.persistence.NamedQuery vs Hibernate's Hibernate's adds cacheable/flush/timeout/readOnly/comment/cache*
cacheable=true Measurably populates and hits the 2nd-level query cache
Record as JPQL constructor target Works directly, JPA 3.2
orm.xml named queries Auto-discovered with no persistence.xml; override same-named annotations
Named query vs inline JPQL speed No measurable difference after warmup (ratio ~1.0-1.1 across runs)
getSingleResultOrNull() Present since JPA 3.2; returns null instead of throwing NoResultException

← Previous: 13 — Date and time mapping | Back to README → | Next: 15 — HQL queries →