The org.springframework.orm.jpa.JpaSystemException: org.hibernate.PersistentObjectException: detached entity passed to persist error means your JPA/Hibernate session has lost track of an entity that you’re trying to save or update.
This usually happens when you’ve retrieved an entity, modified it in a detached state (outside of an active transaction and session), and then tried to persist it again as if it were new, or when you’re trying to re-associate an entity with a different session.
Here are the common culprits and how to fix them:
1. Trying to persist() an entity that’s already managed or detached and needs merge():
- Diagnosis: You’re calling
entityManager.persist(myEntity)whenmyEntitywas previously loaded by the sameEntityManageror has been modified after being detached. - Cause:
persist()is for new, unmanaged entities. If Hibernate knows about the entity (it’s already in the session or was previously persisted),persist()will throw this error. - Fix: If the entity might already be managed or detached but needs to be saved/updated, use
entityManager.merge(myEntity). This method takes a detached entity and returns a managed copy of it, synchronizing its state with the session.// Incorrect: // MyEntity entity = myRepository.findById(id).orElseThrow(); // // ... entity is detached after the method returns ... // entity.setName("New Name"); // entityManager.persist(entity); // Error! // Correct: MyEntity entity = myRepository.findById(id).orElseThrow(); // ... entity is detached after the method returns ... entity.setName("New Name"); MyEntity mergedEntity = entityManager.merge(entity); // Use merge for detached entities // Now mergedEntity is managed and changes will be flushed on commit. - Why it works:
merge()intelligently handles the entity’s state. If it’s new, it’s made persistent. If it’s already managed or detached but known to the session, its state is updated.
2. Modifying a detached entity and trying to save() it with Spring Data JPA:
- Diagnosis: You fetch an entity, close the transaction/session, modify it, and then pass it to a Spring Data JPA repository’s
save()method. - Cause: Spring Data JPA’s
save()method often callspersist()internally if it doesn’t recognize the entity (e.g., if its ID is null or it hasn’t been seen before in the current session). If the entity has an ID and was previously persisted,save()should perform an update (effectively a merge), but issues arise if the entity is truly detached. - Fix: Ensure that modifications happen within an active transaction, or explicitly use
merge()if you’re operating outside a transaction boundary. For Spring Data JPA, the simplest fix is often to ensure thesaveoperation is within a transactional method. If not, fetch again within the transaction or usemerge.@Transactional public void updateEntity(MyEntity entity) { // If entity was detached, it's often better to re-fetch // or explicitly merge if you know it exists. // If entity.getId() is null, save() will persist. // If entity.getId() is not null and it exists, save() will update. // To be safe with detached entities: MyEntity managedEntity = entityManager.merge(entity); // Or if using Spring Data JPA and you know it exists: // MyEntity existingEntity = myRepository.findById(entity.getId()).orElseThrow(); // existingEntity.setName(entity.getName()); // Copy changes // myRepository.save(existingEntity); // This will update } - Why it works:
save()in Spring Data JPA is designed to be flexible. When used within a@Transactionalmethod, it can correctly determine whether topersista new entity ormergean existing one. Explicitly merging provides clearer control.
3. Passing an entity that has been "cleaned up" or invalidated by the session:
- Diagnosis: An entity object was loaded, then the session it belonged to was closed or cleared, and you later try to operate on that entity object.
- Cause: Once a Hibernate session is closed (
session.close()) or cleared (session.clear()), any entities associated with it become detached. If you then try topersist()ormerge()an entity that was previously managed by that now-invalid session, and Hibernate still has a reference to its old internal state (e.g., from a snapshot or identity map), it can get confused. - Fix: Always operate on entities within the scope of an active Hibernate session. If you need to pass entities between methods or layers that might have different session lifecycles, ensure they are re-attached using
merge()or re-fetched within the new session’s transaction.// In one method (session A) MyEntity entity = sessionA.get(MyEntity.class, 1L); // sessionA is closed or cleared. entity is now detached. // In another method (session B) // If you try to persist(entity) it will fail. // You need to merge: MyEntity managedEntity = sessionB.merge(entity); - Why it works:
merge()explicitly tells Hibernate to associate the detachedentitywith the current session (sessionB), creating a managed version.
4. Using different EntityManager instances for an entity’s lifecycle:
- Diagnosis: You load an entity using one
EntityManager(and thus one HibernateSession), then try to persist or merge that exact same entity object using a differentEntityManager. - Cause: JPA
EntityManagerinstances are generally not thread-safe and are tied to specific transactions/sessions. An entity loaded byEntityManager Ais managed bySession A. If you then try to operate on that same object withEntityManager B(which is tied toSession B), Hibernate sees it as a detached object that it doesn’t know how to reconcile withSession B’s current state. - Fix: Ensure that an entity’s lifecycle operations (load, modify, save) occur within the scope of a single
EntityManager(and its underlyingSession) or are explicitly passed between them usingmerge().// Method 1: MyEntity entity = entityManager1.find(MyEntity.class, 1L); // Method 2 (different transaction/entityManager): // If entity was detached from Method 1's session: MyEntity managedEntity = entityManager2.merge(entity); - Why it works:
merge()is the standard way to transfer an entity’s state from one session to another. It creates a managed copy in the target session.
5. Incorrectly handling entity relationships (especially @OneToMany or @ManyToMany):
- Diagnosis: You detach a parent entity, modify its collection of child entities (add new ones, remove existing ones), and then try to persist or merge the parent.
- Cause: If the child entities are also detached or not properly managed, Hibernate can get confused. For example, adding a new, un-persisted child entity to a detached parent’s collection and then trying to
persist()the parent can lead to this error if the child isn’t explicitly persisted or merged first. - Fix: Ensure that all entities involved in a relationship are managed. When adding new entities to a collection,
persist()ormerge()them before adding them to the parent’s collection, or ensure the parent is merged and the cascade settings are correct.// Parent entity might look like: // @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) // private List<ChildEntity> children = new ArrayList<>(); // When detaching, modifying, and re-attaching: ParentEntity detachedParent = repository.findById(parentId).orElseThrow(); ChildEntity newChild = new ChildEntity(); // newChild.setParent(detachedParent); // Set back-reference if needed detachedParent.getChildren().add(newChild); // newChild is transient // To fix: ParentEntity managedParent = entityManager.merge(detachedParent); // This often cascades save new children correctly IF cascade = ALL // OR, more explicitly: // entityManager.persist(newChild); // Persist the new child first // ParentEntity managedParent = entityManager.merge(detachedParent); // Then merge the parent - Why it works: Cascade operations (
CascadeType.ALL,CascadeType.PERSIST,CascadeType.MERGE) tell Hibernate to apply the operation to related entities. Explicitly managing each entity ensures Hibernate knows its state.
6. Using EntityManager.remove() followed by EntityManager.persist() on the same object in the same transaction:
- Diagnosis: You mark an entity for deletion (
remove()) and then, within the same transaction, try topersist()it again. - Cause: This is a logical inconsistency. An entity cannot be both removed and persisted simultaneously within the same session context.
- Fix: Refactor your logic. If you intend to update an entity, do not call
remove(). If you intend to delete it, do not callpersist()ormerge()on it afterward in the same transaction.// Incorrect: // MyEntity entity = entityManager.find(MyEntity.class, id); // entityManager.remove(entity); // entity.setStatus("Restored"); // Modify detached entity // entityManager.persist(entity); // Error! // Correct (if you want to restore it, find it again and update): // MyEntity entityToRemove = entityManager.find(MyEntity.class, id); // entityManager.remove(entityToRemove); // // Later, if you need to 'restore' it, you'd create a NEW entity // // or re-fetch the deleted one (if DB allows and it's not truly gone) // // and persist that NEW or RE-FETCHED entity. - Why it works: It enforces the transactional integrity of the data. Operations within a transaction are atomic; you can’t remove and add the same logical item at once.
The next error you’ll likely encounter after fixing this is a LazyInitializationException if you haven’t correctly handled lazy-loaded collections or relationships, or a NullPointerException if a detached entity was expected to be managed.