Persistence Context and Entity Lifecycle

This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production.

Three older reference books were consulted as bibliography only while preparing these pages and are not the primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

This section’s bibliography lists the reference material consulted while preparing these pages.

Hibernate (JPA & ORM) already introduces the persistence context and a compact entity-lifecycle diagram. This page is the full treatment: every state transition, the operations that cause them, and `merge()’s specific semantics.

The persistence context: identity map + first-level cache

The persistence context is the set of entity instances one EntityManager/Session is currently managing. It serves two roles simultaneously:

  • Identity map — within one persistence context, asking for the same primary key twice (by find, by a query that returns that row, by navigating an association) always returns the same Java object instance. This is what lets equals()-by-reference work correctly for entities loaded through the same context, and why Entities and Identifiers' equality guidance matters once entities cross contexts.

  • First-level cache — a find(Book.class, id) for an id already present in the context returns the cached instance without hitting the database at all. Always on, never configurable, and never shared between EntityManager instances — distinct from the optional, shared second-level cache.

The four entity states

stateDiagram-v2 [*] --> Transient: new Entity() Transient --> Managed: persist(e) [*] --> Managed: find(id) / getReference(id) / query result Managed --> Managed: setters (tracked by dirty checking) Managed --> Removed: remove(e) Removed --> Managed: persist(e) again before flush Removed --> [*]: flush/commit issues DELETE Managed --> Detached: detach(e) / clear() / close() Detached --> Managed: merge(e) returns a NEW managed copy Transient --> [*]: never persisted, garbage collected Detached --> [*]: garbage collected
  • Transient — a plain new Foo(). No database identity, unknown to any persistence context. Never synchronized to the database unless it becomes managed.

  • Managed (persistent) — associated with a context, has a database identity, every field change is tracked by dirty checking (see Flushing & Dirty Checking) and eventually flushed as UPDATE SQL.

  • Detached — was managed; the context closed, or the entity was explicitly evicted. Still carries an identity, but changes to it are no longer tracked or synchronized — it is a plain, disconnected Java object until reattached (via merge) or discarded.

  • Removed — scheduled for deletion. Remains visible in the context (and in the identity map) until the next flush, at which point Hibernate issues the DELETE and the instance transitions out of the persistence context entirely.

EntityManager operations

Operation Effect

persist(entity)

Transient → managed. Schedules an INSERT. With GenerationType.IDENTITY, the INSERT fires immediately (the id is needed right away); with SEQUENCE/TABLE, it can be deferred to flush time.

find(Class, id)

Returns the managed instance for id, hitting the first-level cache first, then a SELECT if not already present. Returns null if no row exists.

getReference(Class, id)

Returns a lazy proxy without querying the database — the proxy resolves its state only when a non-identifier method is first called (and throws EntityNotFoundException then, not immediately, if the row turns out not to exist). Useful for setting a @ManyToOne by id alone, avoiding a SELECT that is thrown away anyway.

merge(entity)

See the dedicated section below — returns a different, managed instance; does not attach the argument.

remove(entity)

Managed → removed. The entity must already be managed — remove() on a detached instance throws IllegalArgumentException (merge it first).

refresh(entity)

Discards the entity’s in-memory state and reloads it from the database, overwriting any unflushed changes —  the inverse of dirty checking, used when the database is known to have changed underneath the current transaction (e.g. after a native bulk update).

detach(entity)

Managed → detached, for that one instance, without ending the whole persistence context.

clear()

Detaches every managed entity in the context at once — more common than one-at-a-time detach(), notably in batch-processing loops (see Bulk Operations & Batching).

merge() semantics, in depth

merge(detachedEntity) is the operation most often used incorrectly. It does not attach the argument you pass to the persistence context. Instead it:

  1. Looks up (or loads) the managed entity with the same identifier.

  2. Copies the state of the detached argument onto that managed instance.

  3. Returns the managed instance — a different object from the one passed in.

Book detached = someCacheOrClientPayload();  // has an id, but from a closed context
Book managed = em.merge(detached);           // NOT detached itself -- a different, managed instance
// detached is still detached and untracked; only `managed`'s changes will be persisted

A common bug is continuing to mutate the argument (detached) after calling merge, expecting those changes to be tracked — they are not; only managed is. If the detached entity has no id yet (was never persisted), merge behaves like persist and creates a new row.

Cascading and exception handling

Cascading state transitions across an association is controlled per-association by cascade (see Associations) — persist, merge, and remove do not automatically propagate to associated entities unless the association declares the matching CascadeType.

Exceptions specific to state mismanagement:

  • OptimisticLockException (JPA) / StaleObjectStateException (native) — a flush detected that the row’s @Version (or state, for versionless optimistic locking) no longer matches what was loaded — someone else committed a conflicting change first. See Locking.

  • LazyInitializationException — a lazy association or proxy was accessed after its owning persistence context closed; there is no session left to issue the SELECT. See Fetching & N+1 for the fetch strategies that prevent it.

  • EntityNotFoundException — a getReference() proxy was accessed but the underlying row no longer exists, or a non-nullable @ManyToOne points at a deleted row.