# 14 — Named queries: what startup validation, caching, and "faster" actually mean [← Previous: 13 — Date and time mapping](13-date-and-time-mapping.md) | [Back to README →](../README.md) | [Next: 15 — HQL queries →](15-hql-queries.md) Backs [ankurm.com: Hibernate 7 named queries](https://ankurm.com/master-hibernate-7-named-queries-clean-efficient-and-maintainable-data-access/). Everything below comes from JUnit tests in [`src/test/java/com/ankurm/hibernatedemo/namedquery/`](../src/test/java/com/ankurm/hibernatedemo/namedquery/) (and one deliberately-broken entity, [`BrokenNamedQueryEmployee`](../src/test/java/com/ankurm/brokenprobe/BrokenNamedQueryEmployee.java), 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`](../src/test/java/com/ankurm/hibernatedemo/namedquery/NamedQueryStartupValidationTest.java) 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`](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`](../src/main/java/com/ankurm/hibernatedemo/namedquery/HibernateExtraEmployee.java): ``` 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](09-testing-in-memory-databases.md#hibernate-jcache-on-the-classpath-turns-on-l2-for-everyone-whether-you-asked-or-not) for why that's off by default repo-wide). Two separate `EntityManager`s, same query, `Statistics` counters: ``` 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`](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`](../src/main/java/com/ankurm/hibernatedemo/namedquery/EmployeeDto.java), on [`NqEmployee`](../src/main/java/com/ankurm/hibernatedemo/namedquery/NqEmployee.java)) works exactly as documented, per [`NamedQueryExecutionTest`](../src/test/java/com/ankurm/hibernatedemo/namedquery/NamedQueryExecutionTest.java): ``` 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: ```java 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`](../src/main/java/com/ankurm/hibernatedemo/namedquery/EmployeeRecordDto.java) constructor is matched exactly like any other multi-argument constructor. Raw output: [`docs/output/namedquery-execution-and-projections.txt`](output/namedquery-execution-and-projections.txt). ## Named queries in `orm.xml` A named query defined purely in [`META-INF/orm.xml`](../src/main/resources/META-INF/orm.xml) (no annotation at all, [`XmlQueryEmployee`](../src/main/java/com/ankurm/hibernatedemo/namedquery/XmlQueryEmployee.java)`.findBySalaryAboveXml`) is picked up automatically by Spring Boot's default JPA bootstrap -- **no `persistence.xml` and no explicit `` 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`](04-annotations-vs-xml.md#ormxml-really-can-define-an-entire-entity-annotation-free). It works side by side with an annotation-defined named query on the same entity, per [`OrmXmlNamedQueryTest`](../src/test/java/com/ankurm/hibernatedemo/namedquery/OrmXmlNamedQueryTest.java): ``` 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`](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`](../src/test/java/com/ankurm/hibernatedemo/namedquery/NamedQueryPerformanceTest.java) 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`](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`](05-jpa-persistence-annotations.md#whats-actually-new-in-jakarta-persistence-32-verified-via-javap-on-jakartapersistence-api-320jar). Raw output: [`docs/output/namedquery-execution-and-projections.txt`](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](13-date-and-time-mapping.md) | [Back to README →](../README.md) | [Next: 15 — HQL queries →](15-hql-queries.md)