DEV Community

Silver_dev
Silver_dev

Posted on

Hibernate Under the Hood. Part 2: Entity Lifecycle and the Persistence Context

This is arguably the most important topic in Hibernate. If you understand entity states and what happens inside the Persistence Context, 80% of Hibernate-related bugs cease to be a mystery.

What is the Persistence Context

The Persistence Context is a container of managed entities within a Session/EntityManager. Its key properties are:

  1. It is a first-level cache. All loaded entities live here until the session is closed.
  2. It guarantees identity. Within a single session, calling get(User.class, 1L) twice will return the exact same Java object instance (Identity Map pattern).
  3. It tracks changes. Upon flush, Hibernate compares the current state of entities with their snapshot and generates an UPDATE only for the modified fields (dirty checking).
  4. It implements write-behind. save/persist do not write to the DB immediately — changes are accumulated and synchronized upon flush.
User u1 = session.get(User.class, 1L);
User u2 = session.get(User.class, 1L);
System.out.println(u1 == u2); // true — the exact same object instance
Enter fullscreen mode Exit fullscreen mode

This is not "optimization for the sake of optimization". It is a semantic guarantee: within a single unit of work, you always work with the same object instance, preventing the scenario of "two objects, both representing User with id=1, one with name='A' and the other with name='B'".

The Four Entity States

1. Transient (New / Unmanaged)

The object is created via new, but Hibernate knows nothing about it.

User user = new User();  // Transient
user.setName("Alice");
Enter fullscreen mode Exit fullscreen mode

This is a regular Java object created with the new operator. It is not associated with the database or the current Hibernate session.

  • Database connection: None. No corresponding record exists in the database.
  • Identifier (ID): Usually null (unless set manually, but Hibernate will ignore it).
  • Tracking: Hibernate does not track changes to its fields. If you modify a field and do nothing else, nothing goes to the DB.
  • Analogy: A document draft written on your desk. It is not yet registered in the archive.

How to transition to another state:

  • To Persistent: call session.save() or session.persist().
  • To Detached: (rarely done directly; usually, the object is saved first, and then the session is closed).

2. Persistent (Managed)

session.persist(user);   // user is now Persistent
user.setName("Bob");     // changes are tracked
// upon flush, Hibernate will generate an UPDATE
Enter fullscreen mode Exit fullscreen mode

The object is managed by the current session. It has a representation in the database (it is already saved there or will be upon flush).

  • Database connection: Direct. The object corresponds to a row in the table.
  • Identifier (ID): Must have a value (the ID from the DB).
  • Tracking: Hibernate closely monitors the object (via Dirty Checking). If you modify any field of this object, Hibernate will notice and automatically generate and execute an UPDATE SQL query on the next flush().
  • Analogy: A document you pulled from the archive, opened in the system, and are editing. The system sees all your changes and will save them when you click "Save" (flush).

How to get into this state:

  • From Transient: session.save(), session.persist().
  • From Detached: session.update(), session.saveOrUpdate(), session.merge().

How to leave this state:

  • To Detached: close the session (session.close()), clear it (session.clear()), or evict the object from the session cache (session.evict()).
  • To Removed: call session.delete() or session.remove().

3. Detached

session.close();
// all previously loaded entities are now Detached
Enter fullscreen mode Exit fullscreen mode

The object was in the Persistent state (it has an ID and exists in the DB), but lost its connection to the current session (the session was closed or the object was evicted from it).

  • Database connection: The object exists in the database.
  • Identifier (ID): Must have a value.
  • Tracking: Hibernate does not track it. If you modify a Detached object's field, nothing goes to the DB until you "re-attach" it to a session.
  • Analogy: A document you physically took from the archive to your desk. You can read and even change it, but the archive system knows nothing about it. To apply changes to the archive, the document must be returned (via merge/update).

How to get into this state:

  • From Persistent: close the session (session.close()), clear the cache (session.clear()), or evict the object (session.evict()).

How to transition to another state:

  • To Persistent: call session.update(), session.saveOrUpdate(), or session.merge() (in a newly opened session).

4. Removed

session.remove(user);  // Removed
// upon flush → DELETE FROM users WHERE id = ?
Enter fullscreen mode Exit fullscreen mode

The object is marked for deletion. It is still associated with the session, but "doomed".

  • Database connection: The record still exists in the DB but is marked for deletion.
  • Tracking: On the next flush() or commit(), Hibernate will generate a DELETE SQL query and remove the row from the database.
  • Analogy: A document stamped with a red "DESTROY" mark. It is still in the system, but at the end of the workday (upon commit), it will be sent to the shredder.

How to get into this state:

  • From Persistent: call session.delete() or session.remove().

Key Takeaway

The most common beginner mistake is attempting to modify a Detached object and expecting it to update in the DB automatically.

Remember: changes are automatically pushed to the DB only from the Persistent state. If an object becomes Detached (e.g., you fetched it from the DB in one method, but are trying to update it in another where the session is already closed), you must explicitly "re-attach" it via merge() or update().

persist() vs save()

Feature persist() (JPA) save() (Hibernate)
Returns void Serializable (the generated ID)
INSERT timing Deferred (upon flush) Can be immediate (if the ID is needed)
Outside a transaction Throws an exception Works (but it's a bad practice)
For detached objects Throws an exception May create a new object (or throw an error if ID exists)

Use persist() if you are working via the JPA API. It is the standard.

merge() vs update()

Feature merge() (JPA) update() (Hibernate)
What it does Loads the current state from the DB, copies changes into a new managed object, and returns it "Re-attaches" the passed detached object to the session
Returns A managed copy void
Original object Remains detached Becomes persistent
If the record was deleted Creates a new record Throws an exception

Crucial note: merge() does not make the passed object managed. It returns a different managed copy.

User detached = ...;               // Detached
User managed = session.merge(detached);
// detached — remains Detached
// managed — is now Persistent
Enter fullscreen mode Exit fullscreen mode

get() vs load()

Feature get() load()
DB query Immediate Lazy (returns a proxy)
If not found Returns null Throws EntityNotFoundException upon access
Returns Real object or null Proxy or real object

load() returns a proxy — Hibernate provides a subclass that will fetch the actual data upon the first access to any field (except the ID).

Other Key Methods

  • refresh(): Reloads the object's state from the DB, overwriting it in memory. Useful if data might have been changed externally (another process, native SQL).
session.refresh(user);  // user is reloaded from the DB
Enter fullscreen mode Exit fullscreen mode
  • evict() / detach(): Removes the object from the Persistence Context. The object becomes Detached.
session.evict(user);  // user is no longer managed
Enter fullscreen mode Exit fullscreen mode
  • clear(): Completely clears the Persistence Context. All objects become Detached.
session.clear();  // 1st-level cache is empty
Enter fullscreen mode Exit fullscreen mode
  • flush(): Synchronizes the Persistence Context with the DB. It does not commit the transaction — it only executes INSERT/UPDATE/DELETE statements.
session.flush();  // SQL is executed, but the transaction is not committed
Enter fullscreen mode Exit fullscreen mode

When does automatic flush occur?

  • Before a transaction commit.
  • Before executing a query that might affect tables containing "dirty" entities (auto-flush).
  • Upon an explicit flush() call.
  • Upon session.close() (if uncommitted, it triggers a rollback).

Dirty Checking — How Hibernate Detects Changes

When loading an entity, Hibernate saves a snapshot — a copy of all its fields. Upon flush, it compares the current state with the snapshot.

@Transactional
public void rename(Long id, String newName) {
    User u = session.get(User.class, id);  // snapshot is saved
    u.setName(newName);                    // modified in memory
    // flush on commit: snapshot.name != u.name → UPDATE users SET name = ?
}
Enter fullscreen mode Exit fullscreen mode

What this provides:

  • No need to call update() — changes are picked up automatically.
  • Generates an UPDATE only for modified fields (unless disabled).
  • If nothing changed, no UPDATE is executed at all.

Dirty checking triggers on flush, not on set. If you modify a field and roll back the transaction, the change won't reach the DB (which is correct). However, if you modify a field and don't commit, it remains in the object's memory.

Write-behind — Why save Doesn't Write Immediately

persist() does not execute an INSERT immediately. It registers the entity in the Persistence Context, and the SQL is generated upon flush.

Why:

  • Batching: Hibernate can collect several INSERTs and send them as a single batch (hibernate.jdbc.batch_size).
  • Operation ordering: Hibernate automatically sequences INSERT/UPDATE/DELETE statements to avoid violating foreign key constraints.
  • Efficiency: If the transaction rolls back, the INSERT was never actually executed against the DB.

Exception: If the ID generation strategy is IDENTITY (auto-increment), Hibernate is forced to execute the INSERT immediately to obtain the generated ID. This breaks batching.

Identity Map — The Uniqueness Guarantee

User u1 = session.get(User.class, 1L);
User u2 = session.get(User.class, 1L);
assert u1 == u2;  // true
Enter fullscreen mode Exit fullscreen mode

How it works: The Persistence Context is essentially a Map<EntityKey, Object>, where the key is (entityClass, id). Upon get, Hibernate first checks the map. If the object is there, it returns it. If not, it loads it from the DB and puts it in the map.

Why it's needed:

  • Consistency guarantee: Prevents having two different objects with the same ID in memory.
  • Performance: Repeated get calls do not hit the DB.
  • Accurate dirty checking: Hibernate knows exactly which object instance to track.

The identity map lives only within a session. Across sessions, these are different objects.

// Session 1
User u1 = session1.get(User.class, 1L);
session1.close();

// Session 2
User u2 = session2.get(User.class, 1L);
assert u1 != u2;  // different object instances, both with id=1
Enter fullscreen mode Exit fullscreen mode

Typical Mistakes and Pitfalls

1) LazyInitializationException

@Transactional
public User getUser(Long id) {
    return session.get(User.class, id);  // returns managed
}
// session is closed, user is now Detached

// in the controller:
user.getOrders().size();  // LazyInitializationException!
Enter fullscreen mode Exit fullscreen mode

Solution: Fetch required data within the transaction (JOIN FETCH, @EntityGraph), or use DTO projections.

2) merge() does not make the original object managed

User detached = ...;
session.merge(detached);
detached.setName("new");  // NOT tracked! detached is still Detached
Enter fullscreen mode Exit fullscreen mode

Solution: Work with the result of merge():

User managed = session.merge(detached);
managed.setName("new");  // NOW tracked
Enter fullscreen mode Exit fullscreen mode

3) Modifying a detached object without merge()

@Transactional
public void update(User user) {  // user came from outside (detached)
    user.setName("new");
    // NOTHING happens — user is not managed
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use session.merge(user) or reload the entity and modify the managed instance.

4) save() vs persist() with an existing ID
If a transient object already has an ID (e.g., assigned manually), save() might assume the object is detached and execute an UPDATE instead of an INSERT. persist() will throw an exception in this situation, which is safer.

5) Long session → Memory Leak
The Persistence Context grows with every loaded entity. If a session is kept open for the entire HTTP request (OpenSessionInView) and loads thousands of objects, memory will leak.

Solution: Do not keep the session open longer than necessary; periodically flush() + clear() during batch processing.

6) flush() does not commit

session.flush();  // SQL is executed, but not committed
// if an exception occurs next — it will roll back
Enter fullscreen mode Exit fullscreen mode

Solution: Remember: flush() is for synchronization, commit() is for finalizing.

7) Auto-flush before queries
Hibernate performs a flush before createQuery() if the query affects tables containing "dirty" entities. This can lead to unexpected UPDATEs in the middle of a method.

Solution: Understand this behavior; if necessary, configure FlushMode.MANUAL or COMMIT.

Example: All States in Action

@Transactional
public void demo() {
    // 1. Transient
    User user = new User();
    user.setName("Alice");

    // 2. Transient → Persistent
    session.persist(user);
    // upon flush → INSERT

    // 3. Persistent (managed)
    user.setName("Bob");  // dirty checking will pick this up on flush

    // 4. Persistent → Detached
    session.evict(user);
    user.setName("Charlie");  // NOT tracked

    // 5. Detached → Persistent (via merge)
    User managed = session.merge(user);
    // managed is now Persistent, user remains Detached

    // 6. Persistent → Removed
    session.remove(managed);
    // upon flush → DELETE
}
Enter fullscreen mode Exit fullscreen mode

Summary

  1. Persistence Context = 1st-level cache + identity map + dirty checking + write-behind. It is the heart of the Session.
  2. Four states: Transient, Persistent, Detached, Removed. Transitions are managed via session methods.
  3. persist() ≠ save(), merge() ≠ update(). Understand the difference.
  4. merge() returns a copy; the original object remains detached.
  5. Dirty checking triggers on flush, not on set.
  6. flush() does not commit; it only synchronizes.
  7. After session closure, objects are Detached → lazy fields are inaccessible.
  8. The identity map lives only within a session — across sessions, objects are different instances.
  9. Long session = memory leak. Do not keep it open longer than necessary.
  10. LazyInitializationException is a symptom of improper state management, not a "Hibernate bug".

Top comments (0)