skip to content

What are you not allowed to do inside a @PreUpdate or @PostPersist callback method on a JPA entity, and which kinds of data changes will never trigger those callbacks at all?

level: seniorimportance: must knowfreq 45%

answer

  1. callbacks run inside flush, on the action queue
  2. no EntityManager/Query calls, no other entities
  3. own-entity mutation OK in PRE, useless in POST
  4. bulk JPQL/native SQL/external writes: no callbacks at all
  5. POST fires before commit -> outbox, not side effects

basics

~20 s

You must not call EntityManager or Query operations, or touch other entity instances — the callback runs mid-flush. And callbacks never fire for bulk JPQL update/delete, native SQL, or changes that dirty checking does not detect on that entity's own columns.

solid answer

~60 s

**Forbidden inside a callback**: invoking `EntityManager` or `Query` operations and accessing other entity instances. The specification forbids it for portability; the underlying reason is that the callback runs while the provider is processing its flush action queue, so re-entering it risks concurrent modification of that queue, re-entrant flushes, or infinite loops. Modifying the *callback's own entity* is fine and is the point of `@PreUpdate` stamping. Throwing an unchecked exception aborts the operation and marks the transaction for rollback. **Never fires**: - bulk JPQL/HQL `update`/`delete` and native SQL — they operate on rows, not on managed entities; - changes made outside this persistence unit entirely (another service, a migration, a DBA); - a 'change' that leaves the entity's own columns identical, because dirty checking issues no UPDATE; - for an owning entity, a modification confined to an inverse-side collection, since none of its columns changed. Also timing: `@PostPersist` and `@PostUpdate` run after the statement but **before commit**, so publishing an external event from them can announce something a rollback then undoes.

code

java · 7 lines
java
// @PreUpdate on Article will NOT run for these rows
em.createQuery("update Article a set a.archived = true where a.year < :y")
  .setParameter("y", 2020)
  .executeUpdate();

// managed copies in the persistence context still say archived = false
em.clear();

go deeper

for a junior

Know that callbacks must not use the EntityManager and that bulk JPQL updates skip them entirely.

for a middle

Explain that callbacks run during flush, which is why re-entering the persistence context is unsupported, and list the change categories that never fire them.

for a senior

Add the commit-timing problem for post callbacks, the collection-only and non-dirty cases, and what that means for an audit scheme's completeness guarantees.

for a principal

Decide where write-path interception belongs at all: callbacks give convenience with holes, database-side mechanisms give completeness without application context — state which guarantee the system actually needs.

## Where a callback actually executes Pre- and post-write callbacks do not run at your call site. They run inside the provider's flush, while it is walking an ordered queue of pending insert, update, delete and collection actions and issuing JDBC statements. Understanding that single fact explains every restriction. ## The restrictions **No EntityManager or Query operations.** Calling `persist`, `merge`, `remove`, `find`, or executing a query from inside a callback is outside the specification. Real failure modes when you do it anyway: adding an action to a queue that is being iterated (concurrent modification), triggering a nested flush while a flush is in progress, or creating a cycle where saving A causes a save of B whose callback saves A. Providers may tolerate some of it, in some versions, for some operations — which is worse than an outright ban, because it works in a unit test and fails under a different flush order in production. **No access to other entity instances.** Same reason: their state may be mid-flush, and reading an uninitialised lazy association may trigger loading at a moment the provider does not expect. **Allowed and intended**: mutating the state of the very entity the callback was invoked for. In `@PrePersist` and `@PreUpdate` those mutations land in the statement being built, which is what makes timestamp stamping work. In the *post* callbacks they do not — the statement is already out; a later flush might pick them up, or nothing will. **Exceptions**: checked exceptions are not permitted in the signature. An unchecked exception propagates out of the operation and the transaction is marked for rollback. That makes `@PreRemove` a viable veto point ('this entity cannot be deleted while it has open orders'), but a crude one — it aborts the whole transaction. ## What never triggers a callback **Bulk JPQL/HQL `update` and `delete`.** These compile to a single SQL statement operating over rows. No entity is loaded, so no callback runs, no version column is bumped unless you write it yourself, and the persistence context can be left holding stale copies of the affected rows. It is the single most common way an audit or timestamp scheme silently develops holes. **Native SQL** executed through the provider or through a raw connection: same, and even more invisible. **Writes from outside the application**: another service, a scheduled job, a migration script, a human with psql. Anything built purely on callbacks is blind to them. **Non-dirty 'changes'.** Hibernate compares the entity against the snapshot taken when it was loaded. A setter that writes an equal value produces no UPDATE and therefore no `@PreUpdate`. Similarly, an entity that was never managed — modified while detached and never merged — never enters the comparison at all. **Collection-only changes on the owning entity.** If you add an element to a `@OneToMany` mapped by the other side, the parent's own columns are unchanged; Hibernate updates the child's foreign key, not the parent row. So the parent's `@PreUpdate` does not fire, and an `updatedAt` scheme that is supposed to reflect 'the aggregate changed' does not move. (`@OptimisticLock` on the collection can be used to force a version bump on the owner, which changes this behaviour deliberately.) **Cascading deletes performed by the database.** If the schema has `ON DELETE CASCADE` and the provider deletes the parent, the children's rows vanish server-side without the provider ever loading them, so no `@PreRemove` runs for them. ## Timing versus commit Post-write callbacks fire after the statement executes but inside the transaction. If you send an email, publish to a message broker, or call another service from `@PostPersist`, and the transaction subsequently rolls back — for a constraint violation later in the same flush, an optimistic-lock failure, or an exception in surrounding code — you have announced an event that never happened. The correct shape for anything with an external side effect is to record the intent inside the transaction (an outbox row) and act after commit; at the Hibernate level, post-commit event listeners exist precisely because callbacks cannot express 'after commit'. ## How to state this in an interview One sentence for the mechanism ('callbacks run inside flush, on managed entities'), then the two consequences: you may not re-enter the persistence context, and anything that changes rows without going through a managed entity is invisible. Then the commit-timing caveat. That is the complete answer, and it is the one that separates someone who has used callbacks from someone who has been burned by them.

  • An audit scheme built on @PreUpdate shows gaps: some rows changed with no audit entry. What are the candidate causes?
    Bulk JPQL or native SQL updates, which never load entities and never fire callbacks; writes from outside the application such as migrations or another service; changes that were not actually dirty, so no UPDATE was issued; and aggregate changes that only touched a child's foreign key, leaving the parent's columns untouched. If completeness is a hard requirement, the write path has to be constrained or the auditing moved to the database.
  • Why is publishing a message from @PostPersist unsafe, and what is the standard alternative?
    @PostPersist runs after the INSERT but before commit, so a later constraint violation, optimistic-lock failure or application exception can roll the transaction back after the message has already been sent. The standard alternative is the transactional outbox: write a row describing the event in the same transaction, and have a separate process publish it after commit, giving at-least-once delivery tied to the data actually being durable.

saying these in an interview costs you the question

  • Calling EntityManager methods from a callback and defending it with 'it works in my tests'
  • Assuming bulk JPQL update statements fire @PreUpdate and bump version columns
  • Publishing external events or sending email from @PostPersist or @PostUpdate, before commit
  • Expecting the parent's @PreUpdate to fire when only its child collection changed
  • Believing a setter call always produces an UPDATE, ignoring snapshot comparison

context