Hibernate & Entity Mapping

The Persistence Context & Entity States

18 min Lesson 6 of 13

The Persistence Context & Entity States

When Hibernate manages your objects, every object lives in a precisely defined state relative to the persistence context — the in-memory workspace that tracks all entity instances known to the current EntityManager. Understanding these states is not optional knowledge: it determines whether your changes are saved, silently ignored, or trigger a lazy-loading exception in production.

What Is the Persistence Context?

The persistence context is the unit of work between your application code and the database. Conceptually it is a map from entity identity (type + primary key) to a live Java object. Every EntityManager instance owns exactly one persistence context. In a typical Spring Boot application the context is scoped to a single transaction: it opens when the transaction begins and is flushed and closed when the transaction commits.

EntityManager vs. persistence context: They are not the same thing. The EntityManager is the API you interact with; the persistence context is the internal cache it maintains. One EntityManager always has exactly one persistence context, but you can configure extended contexts that survive beyond a single transaction.

The Four Entity States

1 — Transient

An object is transient when it has just been constructed with new and has never been associated with any persistence context. Hibernate knows nothing about it. No database row corresponds to it yet, and if you discard the reference the object is garbage-collected with no side effects.

import jakarta.persistence.*; @Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String reference; // getters / setters omitted } // --- Order order = new Order(); // TRANSIENT — no id, no context order.setReference("ORD-2024-001"); // At this point Hibernate has no idea this object exists.

2 — Managed (Persistent)

An object is managed when it has been associated with the current persistence context. This happens via em.persist(entity) (new objects), em.find(), em.merge(), or JPQL queries. While an entity is managed, Hibernate watches it. Before the transaction commits, it compares every managed object's current state to the snapshot it took at load time — a process called dirty checking — and emits the necessary UPDATE statements automatically.

import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @Service public class OrderService { @PersistenceContext private EntityManager em; @Transactional public Order createOrder(String ref) { Order order = new Order(); // transient order.setReference(ref); em.persist(order); // now MANAGED; INSERT will fire at flush return order; } @Transactional public void updateReference(Long id, String newRef) { Order order = em.find(Order.class, id); // MANAGED immediately order.setReference(newRef); // no explicit save needed! // Hibernate detects the change and issues UPDATE at commit } }
No need to call save() inside a transaction. If you fetched or persisted an entity inside the same transaction, any field changes are tracked automatically. This is why Spring Data JPA's save() is only strictly required for transient entities or for detached entities being re-attached; calling it on an already-managed entity is a no-op.

3 — Detached

An object is detached when it previously was managed but its persistence context has closed (the transaction ended, or you called em.detach(entity) or em.clear()). The object still holds its primary key and its last-known field values, but Hibernate is no longer watching it. Changes made to a detached object are silently ignored unless you explicitly re-attach it.

@Transactional public Order loadOrder(Long id) { return em.find(Order.class, id); // Transaction commits here, context closes — returned object is now DETACHED } // Elsewhere, outside any transaction: public void processDetached(Order detachedOrder) { detachedOrder.setReference("UPDATED"); // change is NOT tracked — no transaction, no context // To persist the change, re-attach with merge(): anotherTransactionalMethod(detachedOrder); } @Transactional public Order reattachAndSave(Order detached) { // merge() copies the detached state into a NEW managed instance Order managed = em.merge(detached); return managed; // use this reference — detached is still detached }
Common pitfall with detached entities and lazy loading: If you loaded an entity with a lazily-fetched association inside a transaction, and then you try to access that association outside the transaction (e.g. in the view layer), Hibernate will throw a LazyInitializationException because the persistence context is already closed. Solutions: fetch eagerly with a JOIN FETCH query, use a DTO projection, or enable the Open-Session-in-View pattern (enabled by default in Spring Boot — but understand its trade-offs before relying on it in production).

4 — Removed

An object is removed when you call em.remove(managedEntity) on a managed entity. It remains in the persistence context until the transaction commits, at which point Hibernate fires the DELETE statement. Accessing a removed entity's state before the commit is technically valid but rarely meaningful.

@Transactional public void cancelOrder(Long id) { Order order = em.find(Order.class, id); // MANAGED if (order != null) { em.remove(order); // REMOVED — DELETE fires at commit } }

State Transition Diagram

The four states and their transitions form a well-defined lifecycle:

  • new → managed: em.persist(entity)
  • db row → managed: em.find(), JPQL query, em.merge()
  • managed → detached: transaction ends, em.detach(), em.clear(), em.close()
  • detached → managed: em.merge(detached) returns a new managed copy
  • managed → removed: em.remove(entity)
  • removed → managed: em.persist(removedEntity) before the transaction commits

Why This Matters for Performance

Dirty checking scans every managed entity at flush time. If a single transaction loads hundreds of entities, Hibernate must compare every one of them — even those you never intended to modify. This is a real production performance concern. Strategies to mitigate it:

  • Use read-only transactions (@Transactional(readOnly = true)) for queries. Spring tells Hibernate to skip dirty checking entirely.
  • Prefer DTO projections (JPQL SELECT new com.example.OrderDto(o.id, o.reference) FROM Order o) when you only need data, not entities you intend to update.
  • Call em.detach(entity) explicitly after reading an entity you know you will not modify, to remove it from the dirty-check scan.
Annotate every read-only service method with @Transactional(readOnly = true). It is a free performance gain: Hibernate skips the flush entirely, and some JDBC drivers (and read replicas) can apply additional optimizations.

Flush Modes

Hibernate does not necessarily flush (synchronize the context to the database) only at commit. The default flush mode is AUTO: Hibernate will also flush before executing a JPQL query if the pending changes could affect the query's results. This is correct but can surprise developers who expect changes to be invisible until commit. You can change the flush mode per EntityManager with em.setFlushMode(FlushModeType.COMMIT), but AUTO is the safe default.

Summary

Every entity is always in one of four states: transient (unknown to Hibernate), managed (tracked for changes), detached (known by identity but not tracked), or removed (scheduled for deletion). The transitions between these states are driven by EntityManager operations and transaction boundaries. Mastering this lifecycle is what separates developers who fight Hibernate from those who work with it efficiently.