Cascade Pitfalls

Detached Entity Passed to Persist

If a child already exists in the database but the parent cascades PERSIST to it, Hibernate tries to insert it again:

Example

Category electronics = new Category();
electronics.setId(3L);                     // existing row, but this object is not managed
product.setCategory(electronics);
entityManager.persist(product);            // with cascade = PERSIST on Product.category

Example output

jakarta.persistence.EntityExistsException: detached entity passed to persist: com.shopease.catalog.Category

Fix: remove the cascade on @ManyToOne, and get a reference to the existing row with categoryRepository.getReferenceById(3L) (no SELECT, just a proxy with the id).

Deleting Large Collections

Cascade REMOVE loads every child and deletes them one by one. Deleting a category with 20,000 products would run 20,000 DELETE statements. For bulk deletes use a JPQL bulk statement instead (it skips cascades and the persistence context):

@Modifying
@Query("delete from OrderItem i where i.order.id = :orderId")
int deleteItemsOfOrder(@Param("orderId") Long orderId);

Checklist

  • Cascade only from aggregate root to its own children (Order → OrderItem, Product → ProductImage).
  • No cascade on @ManyToOne; never REMOVE on @ManyToMany.
  • Use orphanRemoval when "removed from the list" should mean "deleted".
  • Use bulk JPQL or native deletes for large data sets.