108 lines
5.4 KiB
Markdown
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)
|