Hibernate Entity Lifecycle: Transient, Managed, Detached

The four Hibernate entity states (transient, managed, detached, removed), the EntityManager method behind each transition, the SQL it sends in Hibernate 7.4, and how to check the state of an object in code.

The Hibernate entity lifecycle is the set of four states an entity object can be in, namely transient (new), managed (persistent), detached and removed. The state depends on whether the object is in the persistence context (the set of objects that one EntityManager or Hibernate Session tracks) and whether it has an id and a database row. The state decides whether Hibernate inserts, updates, deletes or ignores the object at commit.

We need the lifecycle states to understand why a change to one object is saved at commit while the same change to another object is lost, for example after the object left a closed transaction.

The following example moves a ServiceOrder entity of a car garage through all four states.

@Entity
public class ServiceOrder {

  @Id
  @GeneratedValue
  private Long id;

  private String plateNumber;
  private String issue;
  private String status;
  private BigDecimal cost;
}
// 1. Transient
ServiceOrder order = new ServiceOrder("1234", "Brake noise", "open", new BigDecimal("40.00"));
boolean managed = em.contains(order);     // false, order.getId() = null

// 2. Managed
em.persist(order);                        // select next value for ServiceOrder_SEQ
tx.commit();                              // insert into ServiceOrder (cost,issue,plateNumber,status,id) values (?,?,?,?,?)
tx.begin();
order.setStatus("in progress");           // no save() or update() call
tx.commit();                              // update ServiceOrder set cost=?,issue=?,plateNumber=?,status=? where id=?

// 3. Detached
em.close();
order.setCost(new BigDecimal("55.00"));   // detached: no SQL

// 4. Managed again (a copy), then removed
ServiceOrder copy = em2.merge(order);     // select ... from ServiceOrder so1_0 where so1_0.id=?
                                          // commit: update ServiceOrder set ... where id=?
em3.remove(em3.find(ServiceOrder.class, id));   // commit: delete from ServiceOrder where id=?

Notice that the UPDATE after setStatus() runs without any save call, whereas the change after em.close() sends no SQL.

Next, we look at each state and the method that leads to it, and then check the state of an object in code.

1. The Four States of the Hibernate Entity Lifecycle

An entity is a Java class mapped to a table with @Entity. Hibernate keeps the entities it tracks in the persistence context, a map from id to object that exists as long as its EntityManager is open, and each state describes how an object relates to that map and to the database row.

  • A transient object was created with new. Hibernate does not know it, and it has no id and no row.
  • A managed object is in the persistence context. Hibernate compares it with the row at flush and writes the differences.
  • A detached object has an id and in most cases a row, but its persistence context is closed or it was taken out of it.
  • A removed object is still known to the persistence context, but it is scheduled for a DELETE.

A flush is the moment Hibernate sends the pending SQL statements to the database. It happens at commit or when we call em.flush(), and Hibernate also flushes before a query that needs the pending data.

StateIn the persistence context?Has an id?Changes saved at flush?
Transient (new)NoNo (null)No, Hibernate does not know the object
Managed (persistent)YesYes, from persist() onYes: INSERT for a new object, UPDATE for a changed one
DetachedNoYesNo, only after merge()
RemovedYes, marked for deletionYesDELETE

The Jakarta Persistence specification calls the first two states “new” and “managed”, whereas the Hibernate user guide calls them “transient” and “managed or persistent”, but both sets of names mean the same states. Every move between the states comes from one method call.

State diagram of a ServiceOrder: new creates a transient object with id null; persist() and merge() lead from transient to managed; find() and queries load managed objects from the database; detach(), clear() and close() make a managed object detached; merge() returns a managed copy of a detached object; remove() makes a managed object removed, persist() before the flush makes it managed again, and commit sends the DELETE; remove() or persist() on a detached object and merge() on a removed object throw exceptions; only the managed state has contains() = true
Every arrow is one EntityManager method. A detached object comes back only through merge(), which returns a new managed object.

In Hibernate 7.4, each transition causes its own SQL, or none at all.

MethodFromToSQL
em.persist(order)TransientManagedselect next value for ServiceOrder_SEQ, INSERT at flush
em.find(), a JPQL queryDatabase rowManagedSELECT
em.detach(order)ManagedDetachedNone
em.clear()All managed objectsDetachedNone, pending changes are dropped
em.close(), end of runInTransaction()All managed objectsDetachedNone
em.merge(order)DetachedReturns a managed copy, order stays detachedSELECT, then UPDATE at flush
em.merge(order)TransientReturns a managed copy, order stays transientINSERT at flush
em.remove(order)ManagedRemovedDELETE at flush
em.persist(order)RemovedManagedThe DELETE is canceled
em.remove(order)TransientTransient (ignored)None
em.remove(order)DetachedErrorIllegalArgumentException
em.persist(order)DetachedErrorEntityExistsException
em.merge(order)RemovedErrorIllegalArgumentException

2. Entity Lifecycle Example

The following example is a car service garage that runs on Hibernate 7.4.11, Java 25 and an in-memory H2 database. The complete project on GitHub covers every transition from section 1, prints the SQL and includes 24 JUnit tests (mvn -q compile exec:java, mvn test).

2.1. The ServiceOrder Entity

A service order has the car’s plate number, the issue, a status such as “open” or “in progress”, and the cost. @GeneratedValue without a strategy uses a database sequence on H2, so Hibernate fetches the id at persist() and sends the INSERT later, at flush.

@Entity
public class ServiceOrder {

  @Id
  @GeneratedValue
  private Long id;

  private String plateNumber;
  private String issue;
  private String status;
  private BigDecimal cost;
}

We build the EntityManagerFactory in code with HibernatePersistenceConfiguration, without a persistence.xml, and the test data is a single order.

ServiceOrder brakes = new ServiceOrder("1234", "Brake noise", "open", new BigDecimal("40.00"));
emf.runInTransaction(em -> em.persist(brakes));   // brakes is detached after this line

2.2. Checking the State of an Entity in Code

Jakarta Persistence has no getState() method, but three calls together tell the states apart.

  • The call em.contains(entity) returns true only for a managed object. It returns false for transient, detached and removed objects.
  • The call emf.getPersistenceUnitUtil().getIdentifier(entity) returns the id, or null for a transient object with a generated id.
  • Hibernate’s Session.isDirty() returns true when any managed object in the session has changes that are not flushed yet.
boolean before = em.contains(order);                                    // false (transient)
Object idBefore = emf.getPersistenceUnitUtil().getIdentifier(order);   // null
em.persist(order);
boolean after = em.contains(order);                                     // true (managed)
Long id = order.getId();                                                // 1

The method contains() returns false for both a detached and a removed object. Only Hibernate’s internal entry for the object tells them apart, because a removed object still has one, with the status DELETED. The example project wraps all of this in a helper.

public static EntityState stateOf(EntityManager em, Object entity) {
  if (em.contains(entity)) {
    return EntityState.MANAGED;
  }
  EntityEntry entry = em.unwrap(SharedSessionContractImplementor.class)
      .getPersistenceContextInternal().getEntry(entity);
  if (entry != null && entry.getStatus() == Status.DELETED) {
    return EntityState.REMOVED;
  }
  Object id = em.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity);
  return id == null ? EntityState.TRANSIENT : EntityState.DETACHED;
}

The SharedSessionContractImplementor and EntityEntry types are Hibernate’s internal SPI and can change between versions, so we use stateOf() in tests and logging, not in business logic.

2.3. Making a New Object Managed With persist()

The method persist() moves a transient object into the persistence context. The object gets its id right away, but the row appears only at flush.

ServiceOrder order = new ServiceOrder("1234", "Brake noise", "open", new BigDecimal("40.00"));
EntityState before = stateOf(em, order);   // TRANSIENT

em.persist(order);                         // select next value for ServiceOrder_SEQ
EntityState after = stateOf(em, order);    // MANAGED
Long id = order.getId();                   // 1, no INSERT yet

tx.commit();                               // insert into ServiceOrder (cost,issue,plateNumber,status,id) values (?,?,?,?,?)

2.4. Dirty Checking of Managed Entities

A managed entity needs no save or update call, because Hibernate finds the changed fields at flush and sends the UPDATE itself. This check is called dirty checking. When Hibernate loads or inserts an object, it keeps a copy of the field values (a snapshot), and at flush it compares each managed object with its snapshot.

The method em.find() and queries both return managed objects. A query that finds an order already in the persistence context returns that same object.

emf.runInTransaction(em -> {
  ServiceOrder order = em.find(ServiceOrder.class, id);         // select so1_0.id,...,so1_0.status from ServiceOrder so1_0 where so1_0.id=?
  ServiceOrder same = em.createQuery("from ServiceOrder where status = :status", ServiceOrder.class)
      .setParameter("status", "open").getSingleResult();
  boolean identical = same == order;                             // true

  boolean dirtyBefore = em.unwrap(Session.class).isDirty();      // false
  order.setStatus("in progress");
  boolean dirtyAfter = em.unwrap(Session.class).isDirty();       // true
});                                                              // commit: update ServiceOrder set cost=?,issue=?,plateNumber=?,status=? where id=?

By default, Hibernate updates every column, not only status, and a managed object that we only read sends no UPDATE at all. To send the statement before commit, we call flush(), and the change stays part of the open transaction.

order.setStatus("done");
em.flush();                                            // update ServiceOrder set cost=?,issue=?,plateNumber=?,status=? where id=?
boolean dirty = em.unwrap(Session.class).isDirty();    // false

2.5. Detaching an Entity

A detached object keeps its id and field values, but Hibernate no longer compares it with the row. For example, the garage app loads an order in one request, and a mechanic edits it in a form, so the next request gets a detached object. There are three ways to detach a managed object.

  • The call em.detach(order) detaches one object.
  • The call em.clear() detaches every object of the persistence context and drops their unflushed changes.
  • The call em.close() ends the persistence context. The method emf.runInTransaction() closes its EntityManager at the end, so every entity it returns or touches is detached afterwards.

Changes to a detached entity are ignored, so commit sends no SQL for them. Calling find() again creates a second object loaded from the row.

emf.runInTransaction(em -> {
  ServiceOrder order = em.find(ServiceOrder.class, id);
  em.detach(order);
  EntityState state = stateOf(em, order);                 // DETACHED
  order.setStatus("lost");                                // no effect on the row

  ServiceOrder again = em.find(ServiceOrder.class, id);   // select ... where so1_0.id=?
  boolean same = again == order;                          // false
  String status = again.getStatus();                      // "open"
});                                                       // commit: no SQL

The methods clear() and close() behave the same way, and a change made before clear() is lost too, because no flush happened before it.

2.6. Reattaching a Detached Entity With merge()

The method merge() brings a detached object back, but it does not attach the object we pass. Instead, it loads (or finds) the managed instance with the same id, copies our field values onto it and returns that instance.

merge() of a detached ServiceOrder with id 1 and cost 55.00: Hibernate runs a SELECT and loads a managed copy with cost 40.00 into the persistence context, copies the cost 55.00 onto it, returns the copy and sends an UPDATE at commit; the original order stays detached and copy == order is false
The method merge() copies our values onto a managed instance and returns it. The object we passed stays detached.
order.setCost(new BigDecimal("55.00"));              // order is detached, no SQL

ServiceOrder copy = emf.callInTransaction(em -> {
  ServiceOrder managed = em.merge(order);            // select so1_0.id,...,so1_0.status from ServiceOrder so1_0 where so1_0.id=?
  boolean same = managed == order;                   // false
  EntityState orderState = stateOf(em, order);       // DETACHED
  EntityState managedState = stateOf(em, managed);   // MANAGED
  return managed;
});                                                  // commit: update ServiceOrder set cost=?,issue=?,plateNumber=?,status=? where id=?

Always continue with the object that merge() returns. A change to order after the merge() call is not saved, because order is still detached. The method merge() also accepts a transient object, and it inserts a managed copy, so only the copy gets the id.

2.7. Removing an Entity and Undoing the Removal

The method remove() marks a managed object for deletion. The DELETE is sent at flush, and the Java object keeps its id.

emf.runInTransaction(em -> {
  ServiceOrder order = em.find(ServiceOrder.class, id);
  em.remove(order);
  boolean managed = em.contains(order);     // false
  EntityState state = stateOf(em, order);   // REMOVED
  Long orderId = order.getId();             // 1
});                                         // commit: delete from ServiceOrder where id=?

Until the flush, we can undo the removal, because persist() on a removed object makes it managed again, and commit sends neither a DELETE nor an INSERT.

em.remove(order);                  // REMOVED
em.persist(order);                 // MANAGED again
                                   // commit: no DELETE, the row stays

2.8. What Flush and Commit Do in Each State

At flush, Hibernate goes through the persistence context and writes one statement per object that needs it, and it never visits objects outside the persistence context.

At commit, Hibernate walks the persistence context: a new order passed to persist() gets an INSERT, a managed order with a changed status gets an UPDATE, an unchanged managed order gets no SQL, a removed order gets a DELETE; a detached order with a changed cost and a transient order that was never persisted are outside the persistence context and get no SQL
Only objects in the persistence context reach the database. Detached and transient objects are skipped, whatever their fields contain.
Object at flushSQL
Transient, never passed to persist()None
Managed, new (passed to persist())INSERT
Managed, changed since it was loadedUPDATE
Managed, unchangedNone
Detached, changedNone
RemovedDELETE

3. Entity Lifecycle FAQs

3.1. Why Does remove() Fail for a Detached Entity?

Because remove() accepts only a managed object. Our brakes order is detached after the transaction that saved it, so removing it in a new transaction fails.

emf.runInTransaction(em -> em.remove(brakes));
java.lang.IllegalArgumentException: org.hibernate.DetachedObjectException: Given entity is not associated with the persistence context

We first get the managed instance, with merge() or find(), and remove that one.

emf.runInTransaction(em -> em.remove(em.merge(brakes)));                        // select ..., delete from ServiceOrder where id=?
emf.runInTransaction(em -> em.remove(em.find(ServiceOrder.class, brakes.getId())));

3.2. Can We Call persist() on a Detached Entity?

No, because persist() is only for objects without a row, and a detached object already has an id, so Hibernate 7 throws EntityExistsException.

jakarta.persistence.EntityExistsException: Detached entity passed to persist: com.howtodoinjava.hibernate.states.ServiceOrder

We use merge() for a detached object. We call persist() for new objects and merge() for detached ones, whereas managed objects need no call at all.

3.3. What Replaced save(), update() and saveOrUpdate() in Hibernate 7?

Hibernate 6.0 deprecated the old Session methods and Hibernate 7.0 removed them, so calls to them no longer compile. Each one maps to a Jakarta Persistence method from section 2.

Removed in Hibernate 7Use instead
session.save(order)em.persist(order)
session.update(order)em.merge(order), and keep the returned copy
saveOrUpdate()persist() for a transient object, merge() for a detached one
session.delete(order)em.remove(order)

The method session.evict(order) still exists in Hibernate 7 and does the same as em.detach(order).

3.4. Is an Entity Returned by a Query Managed?

Yes. Entities returned by find() and by JPQL queries are managed, and dirty checking applies to them. If the persistence context already holds an object with the same id, the query returns that object instead of a new one, as the identical check in section 2.4 shows. Scalar results, such as a list of plate numbers, are plain values and have no state.

3.5. Does a Removed Entity Keep Its Id?

Yes. After remove() and after the commit, order.getId() still returns 1, because the Java object stays in memory with all its values and only the row is gone. For another EntityManager, the object looks like a detached one (contains() is false, the id is set), so we do not reuse it after the commit.

4. Conclusion

Each Hibernate entity is transient, managed, detached or removed, and the state decides what the flush does with it. Managed objects are saved through dirty checking, detached objects are ignored until we pass them to merge() and continue with the returned copy, and removed objects become a DELETE unless persist() takes them back. The call em.contains() together with the id tells us the state in code. Code that reacts to these transitions can use the JPA lifecycle callbacks such as @PreUpdate.

5. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi Lokesh,

    I’ve a Entity which is designed to be immutable. How does hibernate load data into immutable entity objects? Please correct me if my question is wrong. Would you please send me the response to my given mail id?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.