skip to content

You call EntityManager.remove(entity) on a managed entity inside a transaction. What state is that instance in afterwards, when does the DELETE statement actually reach the database, and what happens if you call persist() on the same instance before the transaction commits?

level: middleimportance: should knowfreq 50%

answer

  1. remove() queues, flush executes
  2. removed = still in context, DELETE pending
  3. persist() on removed cancels the delete
  4. remove(detached) -> IllegalArgumentException; remove(transient) ignored
  5. delete-then-insert same key: flush in between

basics

~20 s

It becomes removed: still in the persistence context, deletion queued. The DELETE runs at the next flush — an explicit flush, a query that forces one, or commit. Calling persist() on it before then cancels the removal and makes it managed again.

solid answer

~50 s

`remove()` does not execute SQL. It moves the managed instance to the **removed** state: still registered in the persistence context, its fields still readable, with a delete action queued. The DELETE is emitted at the next flush — explicit `flush()`, a query that triggers an automatic flush, or transaction commit. Before that flush, JPA defines `persist()` on a removed instance as *cancelling* the removal, returning it to managed, so nothing is deleted. Two edge rules matter: `remove()` on a *transient* instance is ignored, and `remove()` on a *detached* instance throws `IllegalArgumentException` (or fails at commit) — you must `find()` or `merge()` it first. Cascade REMOVE and `orphanRemoval = true` put children into the same removed state. After the delete has taken effect, the object is just a plain object with no row behind it; re-persisting it is an INSERT, not an undelete.

code

java · 9 lines
java
Order o = em.find(Order.class, 42L); // managed
em.remove(o);                        // removed; no SQL yet
System.out.println(o.getTotal());    // still readable
em.persist(o);                       // removal cancelled -> managed again
em.flush();                          // no DELETE is emitted

Order d = detachedOrderFromUi;
em.remove(d);                        // IllegalArgumentException
em.remove(em.find(Order.class, d.getId())); // correct

go deeper

for a junior

Say that remove() schedules a delete that runs at flush/commit and that you must remove a managed instance, not a detached one.

for a middle

Add the persist-cancels-removal rule, the transient/detached edge cases, and cascade REMOVE versus orphanRemoval.

for a senior

Bring in flush ordering (inserts before deletes) and the delete-then-recreate constraint trap, plus when a bulk JPQL delete is the right tool and what it costs.

for a principal

Discuss deletion strategy at system level: hard delete versus soft delete, referential-integrity ownership between application and schema, and the operational cost of cascade chains on large graphs.

## remove() is a scheduling call, not a SQL call Hibernate batches work. `EntityManager.remove(entity)` marks a managed instance as **removed** and appends a delete action to the session's action queue. The instance stays in the persistence context — you can still read its fields, and it still occupies its slot in the identity map — but Hibernate now intends to delete its row. The SQL DELETE is issued at the next **flush**, which happens in three ways: you call `flush()` yourself; you run a query and the flush mode causes Hibernate to synchronise first so the query sees your pending work; or the transaction commits. This deferral is what allows Hibernate to order statements sensibly (inserts before updates before deletes within its own ordering rules) and to batch them. ## What you may and may not do to a removed instance JPA specifies the behaviour of each operation against a removed instance: - `persist(removedInstance)` — **cancels the removal**. The instance goes back to managed and no DELETE is executed. This is the one genuine "undo", and it only works before the delete has been flushed. - `remove(removedInstance)` — ignored, it is already scheduled. - `merge(removedInstance)` — an `IllegalArgumentException`. - `refresh(removedInstance)` — an `IllegalArgumentException`. - `detach(removedInstance)` — the instance leaves the context; the pending delete is not cascaded further. Modifying fields of a removed entity is pointless: dirty checking will not produce an UPDATE for a row that is about to disappear (Hibernate will not emit an UPDATE followed by a DELETE for the same instance in the ordinary case). ## Removing something the context doesn't manage - **Transient instance**: `remove()` is defined as ignored. Nothing was ever persisted, so there is nothing to delete. - **Detached instance**: `remove()` throws `IllegalArgumentException` — or, depending on the provider and timing, the transaction fails at commit. The persistence context has no idea whether that instance's data is current, so it refuses. The correct pattern is `em.remove(em.find(Order.class, id))`, or `em.remove(em.merge(detached))` when you already hold the object. `find()` is preferable: it avoids copying a possibly stale object's state into the context just to delete it. ## Cascades and orphan removal `cascade = CascadeType.REMOVE` propagates the removed state to associated entities at the time of the call, so children are queued for deletion too. `orphanRemoval = true` is different in trigger: removing a child from the parent's collection makes Hibernate schedule a delete for that child at flush, even though you never called `remove()` on it. Both end at the same state. Note that a bulk `DELETE` in JPQL bypasses all of this — it does not consult or update the persistence context, so instances already loaded stay managed and stale, and cascades are not applied. ## Order of statements at flush Hibernate's default flush ordering executes inserts, then updates, then collection operations, then deletes. That is a frequent source of constraint violations in the delete-then-insert-same-key pattern: within a single flush, deleting a row and inserting another with the same unique key can fail because the INSERT is executed *first*. The remedy is an explicit `flush()` between the two operations, which forces the delete out to the database before the insert is queued. ## After the delete Once the DELETE has run and the transaction commits, no row corresponds to the object anymore. The instance is no longer managed and is effectively an ordinary object again. If you call `persist()` on it now, with a generated identifier Hibernate will assign a new one and INSERT a new row; with an assigned identifier it will INSERT under the old key. Either way it is a fresh row, not a resurrection — anything that referenced the old row by id is not repaired. ## Practical checklist - Deleting inside a loop and expecting each DELETE immediately? Add `flush()` (and `clear()`) periodically, or the action queue grows. - Deleted rows still visible to a query in the same transaction? Your flush mode or the query type is skipping the flush — force it. - `IllegalArgumentException: Removing a detached instance`? Load it first inside the transaction. - Constraint violation on a delete-then-recreate? Flush between the two.

  • How does a bulk 'DELETE FROM Order o WHERE o.status = :s' JPQL statement differ from calling remove() on each entity?
    A bulk delete is translated into a single SQL DELETE and executed directly. It ignores the persistence context: cascades and orphan removal are not applied, lifecycle callbacks such as @PreRemove do not fire, and entities already loaded remain managed with stale state pointing at rows that no longer exist. You normally flush before it and clear the context after it. It is far faster for large sets, which is the trade-off.
  • You removed a parent with cascade REMOVE and still got a foreign-key violation. Why might that happen?
    Cascade REMOVE only reaches associations that Hibernate knows about and that are loaded or mapped for cascading. Rows referencing the parent through a mapping without the cascade, through a join table Hibernate did not manage, or from another entity type entirely are untouched, so the database rejects the parent delete. Either extend the cascade/orphanRemoval mapping, delete the referencing rows explicitly, or rely on ON DELETE CASCADE in the schema.

saying these in an interview costs you the question

  • Claiming remove() issues the DELETE immediately.
  • Thinking persist() after remove() creates a duplicate row rather than cancelling the deletion.
  • Calling remove() on an object that came from the UI layer without reloading it first.
  • Assuming a JPQL bulk delete keeps the persistence context in sync.
  • Expecting deletes to be executed in the order you wrote them relative to inserts within one flush.

context