Does a JPQL statement DELETE FROM Order o WHERE o.createdAt < :cutoff, executed with executeUpdate(), also remove the child rows mapped with cascade = REMOVE or orphanRemoval? What typically goes wrong if you assume it does?
answer
- no graph walk, no cascade
- FK violation is the lucky failure
- orphan rows are the unlucky one
- children first, bottom-up bulk deletes
- or ON DELETE CASCADE in the schema
basics
~20 sNo. A bulk DELETE emits SQL against the entity's own table only; cascade and orphanRemoval are object-layer features that are skipped. Children remain, and foreign keys usually make the statement fail with a constraint violation.
solid answer
~50 sBulk statements never materialise entities, so JPA cascade settings and orphanRemoval - which Hibernate applies when it flushes a managed object graph - simply do not run. A bulk DELETE deletes rows from the entity's table (plus its secondary or joined tables via an id-table strategy) and nothing else. The usual symptom is a foreign key constraint violation from the child table, or, when the FK is missing or nullable, orphaned child rows and dangling join-table entries that no one notices until a report goes wrong. The fixes, in order of preference: delete children first with their own bulk statements, innermost table outwards, in one transaction; or define ON DELETE CASCADE in the schema and let the database do it; or, when the row count is small and callbacks matter, load the entities and call remove() so the normal cascade machinery runs. Also remember bulk deletes fire no @PreRemove callbacks and leave already-loaded parents managed and stale.
code
java · 10 linesem.createQuery("delete from OrderLine l where l.order.id in "
+ "(select o.id from Order o where o.createdAt < :cutoff)")
.setParameter("cutoff", cutoff)
.executeUpdate();
em.createQuery("delete from Order o where o.createdAt < :cutoff")
.setParameter("cutoff", cutoff)
.executeUpdate();
em.clear();go deeper
State plainly that bulk deletes hit one table and ignore cascade, and that children must be deleted first.
Explain why - no entity instances, so no graph walk - and describe both failure modes, constraint violation and silent orphans.
Compare child-first bulk statements, database ON DELETE CASCADE and entity removal, including chunking on large data sets and clearing the persistence context.
Decide where deletion semantics live - application code versus schema constraints - and make retention or purge jobs an explicit, tested part of the data lifecycle.
## Two different deletion mechanisms JPA has two ways to delete, and they share almost nothing. Entity removal is the object-layer path: em.remove(order) marks the managed instance for deletion; at flush Hibernate walks the association graph, applies cascade = REMOVE and orphanRemoval, fires @PreRemove/@PostRemove callbacks, deletes collection rows and join-table rows, and orders the statements so children go before parents. Bulk deletion is the statement path: a JPQL DELETE is translated to SQL and executed. Hibernate knows the entity's tables and the WHERE clause and nothing more. There is no graph walk because there is no graph. ## What that means concretely Given Order with @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) List<OrderLine> lines, the bulk statement 'delete from Order o where o.createdAt < :cutoff' emits roughly `delete from orders where created_at < ?`. The order_line rows are untouched. If order_line.order_id has a foreign key with the default NO ACTION / RESTRICT behaviour, the database rejects the DELETE and Hibernate surfaces a constraint violation exception. Many developers meet the feature this way, which is the lucky case: the database protected them. The unlucky case is a schema without that constraint, or one where the FK column is nullable with ON DELETE SET NULL. Then the statement succeeds and leaves orphan rows behind - storage that grows forever, reports that double count, and later imports that trip over ids pointing at nothing. The same applies to many-to-many join tables and element-collection tables: those rows belong to a collection table that a bulk delete on the owning entity does not consider at all. ## Doing it correctly Delete children first. Issue bulk deletes bottom-up in one transaction: first 'delete from OrderLine l where l.order.id in (select o.id from Order o where o.createdAt < :cutoff)', then the delete on Order itself. This keeps the set-based performance and satisfies the constraints. Watch that the subquery is evaluated per statement - on large data sets, collect ids into a staging table or process in chunks. Let the database cascade. ON DELETE CASCADE on the FK moves the work into the engine, and a single bulk delete on the parent then removes children too. It is fast and correct, but it is invisible from the Java side, it still fires no callbacks, and it can delete far more than expected if the graph is deep - so it is a schema decision to make deliberately. Use entity removal for small sets. If the number of rows is modest and you need @PreRemove hooks, auditing or business validation, load and remove(). You pay N statements but you get the object-layer semantics. ## The other bypassed behaviours Beyond cascades, a bulk delete skips lifecycle callbacks and entity listeners, and it ignores optimistic version checks - it deletes whatever matches the WHERE clause regardless of concurrent modification. It also leaves the persistence context alone: an Order you loaded earlier stays managed after its row is gone, and flushing a modification of that instance can produce a confusing update-count-mismatch failure. Clear the persistence context after bulk deletes for the same reason you do after bulk updates. Finally, for entities in a joined inheritance hierarchy or with secondary tables, Hibernate cannot express the delete as one statement; it selects the matching ids into a temporary id table and deletes from each table in turn. That is why a one-line bulk delete can produce several statements plus temporary-table DDL in the SQL log. ## Rule of thumb Bulk statements are SQL with entity names. Anything that lives in the object layer - cascade, orphanRemoval, callbacks, versioning, the persistence context - is not part of the deal, and you either re-express it in SQL or in the schema.
- Your bulk delete succeeds in the test database but fails in production with a foreign key violation. What is the most likely difference?The production schema declares the child foreign key while the test schema does not, or was generated without constraints. The application code was always wrong - it relied on cascade settings that bulk statements ignore - and only the constraint made the defect visible. Fix the statement order rather than dropping the constraint.
- When would you still prefer loading entities and calling remove() over a bulk delete?When the row count is small and the object layer's behaviour matters: @PreRemove callbacks, auditing, domain validation, cascading across a complex graph, or optimistic version checks. The per-row cost is acceptable for tens or hundreds of rows and you get semantics that a bulk statement cannot reproduce without hand-written SQL.
saying these in an interview costs you the question
- Believing cascade = REMOVE applies to JPQL bulk deletes
- Solving the constraint violation by dropping the foreign key instead of deleting children first
- Assuming orphanRemoval covers join-table or element-collection rows in a bulk delete
- Expecting @PreRemove or version checks to run
- Forgetting to clear the persistence context, leaving managed entities whose rows no longer exist