Files
hibernate-demo/docs/24-interceptors.md
T

108 lines
5.4 KiB
Markdown

# 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)