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
- Ehcache configured explicitly for this test, since it is off by default -- see
chapter 09's writeup of the classpath-pollution trap
for why that's off by default repo-wide). Two separate
EntityManagers, same query,Statisticscounters:
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 →