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) when myEntity was previously loaded by the same EntityManager or 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 calls persist() 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 the save operation is within a transactional method. If not, fetch again within the transaction or use merge.
    @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 @Transactional method, it can correctly determine whether to persist a new entity or merge an 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 to persist() or merge() 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 detached entity with 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 Hibernate Session), then try to persist or merge that exact same entity object using a different EntityManager.
  • Cause: JPA EntityManager instances are generally not thread-safe and are tied to specific transactions/sessions. An entity loaded by EntityManager A is managed by Session A. If you then try to operate on that same object with EntityManager B (which is tied to Session B), Hibernate sees it as a detached object that it doesn’t know how to reconcile with Session 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 underlying Session) or are explicitly passed between them using merge().
    // 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() or merge() 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 to persist() 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 call persist() or merge() 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.

Want structured learning?

Take the full Spring-boot course →