Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# 24 — Interceptors
|
||||
|
||||
[← Previous: 23 — Pagination](23-pagination.md) | [Next: 25 — Hibernate Search →](25-hibernate-search.md) | [Back to README →](../README.md)
|
||||
|
||||
Backs the rewrite of ankurm.com post 4891 (interceptors).
|
||||
|
||||
## `Interceptor` is all default methods now — extending `EmptyInterceptor` is obsolete
|
||||
|
||||
`javap` against `org.hibernate.Interceptor` in `hibernate-core-7.4.5.Final.jar` shows every
|
||||
method (`onSave`, `onFlushDirty`, `onDelete`, `onLoad`, `findDirty`, and the rest) declared
|
||||
`default`. There is no longer a reason to extend a no-op base class just to override one or two
|
||||
callbacks — implement `Interceptor` directly:
|
||||
|
||||
```java
|
||||
public class UppercasingInterceptor implements Interceptor {
|
||||
@Override
|
||||
public boolean onFlushDirty(Object entity, Object id, Object[] currentState,
|
||||
Object[] previousState, String[] propertyNames, Type[] types) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
[`UppercasingInterceptor.java`](../src/main/java/com/ankurm/hibernatedemo/interceptor/UppercasingInterceptor.java)
|
||||
|
||||
The old public `org.hibernate.EmptyInterceptor` base class **still exists in the 7.4.5.Final
|
||||
jar** — but only as `org.hibernate.internal.EmptyInterceptor`: a `final`, singleton-only class
|
||||
(`public static final Interceptor INSTANCE`) that isn't meant to be extended by application code
|
||||
any more. It moved from a public API class to an internal implementation detail, confirmed
|
||||
by `javap`, not by assuming it was simply deleted.
|
||||
|
||||
Also note the identifier parameter type: `onSave`/`onFlushDirty`/`onDelete`/`onLoad` all take
|
||||
`Object id`, not `java.io.Serializable id` — Hibernate 6 widened this across the whole interface,
|
||||
since an application is free to use a non-`Serializable` identifier type.
|
||||
|
||||
## The state-array-mutation contract
|
||||
|
||||
```java
|
||||
private boolean uppercaseNameIfPresent(Object[] state, String[] propertyNames) {
|
||||
for (int i = 0; i < propertyNames.length; i++) {
|
||||
if ("name".equals(propertyNames[i]) && state[i] instanceof String s) {
|
||||
String upper = s.toUpperCase(Locale.ROOT);
|
||||
if (!upper.equals(s)) {
|
||||
state[i] = upper;
|
||||
return true; // tells Hibernate: yes, I changed the state array, flush it
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
The boolean return value **is** the contract: `true` tells Hibernate the `state` array was
|
||||
actually mutated, so the (possibly changed) values get flushed to the database; `false` (or
|
||||
returning without touching `state`) leaves the original values untouched. Look the property up
|
||||
by name in `propertyNames` — the array's index order is Hibernate's internal property ordering,
|
||||
not necessarily the entity's declaration order.
|
||||
[Test](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) —
|
||||
[output](output/24-session-scoped-mutation.txt)
|
||||
|
||||
## Session-scoped vs. globally-registered interceptors
|
||||
|
||||
```java
|
||||
sessionFactory.withOptions().interceptor(myInterceptor).openSession();
|
||||
```
|
||||
scopes the interceptor to that one `Session` — a plain `sessionFactory.openSession()` elsewhere
|
||||
is completely unaffected.
|
||||
[Test](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) —
|
||||
[output](output/24-interceptor-scoping.txt)
|
||||
|
||||
To register one for *every* `Session` a `SessionFactory` ever opens, set
|
||||
`hibernate.session_factory.interceptor` once, at `SessionFactory` build time — this is exactly
|
||||
the property a Spring Boot `HibernatePropertiesCustomizer` bean sets under the hood when it calls
|
||||
`properties.put("hibernate.session_factory.interceptor", interceptor)`:
|
||||
|
||||
```java
|
||||
new StandardServiceRegistryBuilder()
|
||||
.applySetting("hibernate.session_factory.interceptor", globalInterceptor)
|
||||
.build();
|
||||
```
|
||||
Demonstrated here on a standalone, non-Spring registry deliberately — registering a global
|
||||
interceptor on this repo's *shared* Spring-managed `SessionFactory` would retroactively affect
|
||||
every other chapter's tests that reuse the same cached Spring context.
|
||||
[Test](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) —
|
||||
[output](output/24-global-via-property.txt)
|
||||
|
||||
## Bulk HQL updates bypass interceptor callbacks entirely
|
||||
|
||||
```java
|
||||
session.createMutationQuery("update Task set name = 'renamed by bulk update' where id = :id")
|
||||
.setParameter("id", id)
|
||||
.executeUpdate();
|
||||
```
|
||||
`onFlushDirty` never fires for this. A bulk HQL (or native SQL) mutation changes rows directly in
|
||||
the database via a single `UPDATE`/`DELETE` statement — it never loads a managed entity instance
|
||||
into the persistence context, and `onFlushDirty` needs a managed entity's dirty state to fire
|
||||
against in the first place. Anything an interceptor is relied on for (auditing, denormalized
|
||||
field maintenance) has to be handled separately for bulk operations.
|
||||
[Test](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) —
|
||||
[output](output/24-bulk-update-bypass.txt)
|
||||
|
||||
## Going deeper
|
||||
|
||||
- `StatelessSession` never calls interceptor callbacks at all, by design — it exists specifically
|
||||
to skip persistence-context machinery for bulk-style work.
|
||||
- An interceptor that mutates unrelated fields on every flush is a subtle source of extra
|
||||
`UPDATE` statements — mutate only when the value actually needs to change, exactly as this
|
||||
chapter's `uppercaseNameIfPresent` checks `!upper.equals(s)` before returning `true`.
|
||||
- [Hibernate ORM 7.4 User Guide — interceptors](https://docs.jboss.org/hibernate/orm/7.4/userguide/html_single/Hibernate_User_Guide.html#events-interceptors)
|
||||
Reference in New Issue
Block a user