Bulk updates and the stale persistence context
A bulk UPDATE bypasses everything the ORM knows, including entities already loaded.
Open this lesson in the learning hubKey points
- A JPQL bulk
updateordeleteis translated straight to SQL. It does not load entities, does not run callbacks, and does not update the persistence context. - That means entities already loaded in the same context are now stale, and a subsequent flush can write the old values back over the bulk change.
clearAutomatically = trueon@Modifyingclears the context afterwards, which prevents that - at the cost of detaching everything, so unsaved changes are lost.- Bulk operations also skip
@PreUpdate, auditing, optimistic locking and cascade. A bulk delete will not cascade to children and can leave orphans or violate a foreign key. - They are still the right tool for large changes: updating a million rows one entity at a time is orders of magnitude slower and will exhaust the persistence context.
- The rule of thumb: use bulk for genuine bulk work, run it in its own transaction, and do not mix it with entity manipulation in the same unit of work.
Example
// Bypasses the ORM entirely. Fast, and dangerous in the wrong place.
interface OrderRepository extends JpaRepository<Order, Long> {
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("update Order o set o.status = :to where o.status = :from")
int bulkUpdateStatus(@Param("from") OrderStatus from, @Param("to") OrderStatus to);
}
// flushAutomatically - pending changes are written BEFORE the bulk runs,
// so the bulk sees them
// clearAutomatically - the context is cleared AFTER, so nothing stale
// survives to be flushed back
// THE BUG this prevents:
@Transactional
void broken() {
Order order = repo.findById(1L).orElseThrow(); // status = NEW, now managed
repo.bulkUpdateStatus(NEW, PROCESSING); // DB row -> PROCESSING
// order.status is still NEW in memory. Dirty checking at commit sees a
// difference and writes NEW back - silently undoing the bulk update.
order.setNote("touched");
}
/*
* WHAT BULK OPERATIONS SKIP:
*
* @PreUpdate / @PostUpdate not called
* @Version optimistic locking not incremented -> lost update detection off
* cascade NOT applied - children are left behind
* second-level cache stale unless evicted
* Envers auditing no revision recorded
*
* So a bulk delete of parents can leave orphan children, or fail on a
* foreign key, depending on the constraint.
*
* WHEN TO USE THEM ANYWAY:
*
* 1,000,000 rows, entity by entity minutes, and the context explodes
* 1,000,000 rows, one bulk UPDATE one statement
*
* Do it in its own transaction, and do not touch entities in it.
*/
Bulk statements bypass callbacks, versioning and cascade, and leave loaded entities stale - isolate them in their own transaction.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Hibernate course, and every lesson in it is listed on the Hibernate contents page.