211 lines
12 KiB
Markdown
Executable File
211 lines
12 KiB
Markdown
Executable File
# 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 `<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`](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)
|