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:
- It is a first-level cache. All loaded entities live here until the session is closed.
- It guarantees identity. Within a single session, calling
get(User.class, 1L)twice will return the exact same Java object instance (Identity Map pattern). - It tracks changes. Upon
flush, Hibernate compares the current state of entities with their snapshot and generates anUPDATEonly for the modified fields (dirty checking). - It implements write-behind.
save/persistdo not write to the DB immediately — changes are accumulated and synchronized uponflush.
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
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");
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()orsession.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
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
UPDATESQL query on the nextflush(). - 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()orsession.remove().
3. Detached
session.close();
// all previously loaded entities are now Detached
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(), orsession.merge()(in a newly opened session).
4. Removed
session.remove(user); // Removed
// upon flush → DELETE FROM users WHERE id = ?
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()orcommit(), Hibernate will generate aDELETESQL 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()orsession.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
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
-
evict()/detach(): Removes the object from the Persistence Context. The object becomes Detached.
session.evict(user); // user is no longer managed
-
clear(): Completely clears the Persistence Context. All objects become Detached.
session.clear(); // 1st-level cache is empty
-
flush(): Synchronizes the Persistence Context with the DB. It does not commit the transaction — it only executesINSERT/UPDATE/DELETEstatements.
session.flush(); // SQL is executed, but the transaction is not committed
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 = ?
}
What this provides:
- No need to call
update()— changes are picked up automatically. - Generates an
UPDATEonly for modified fields (unless disabled). - If nothing changed, no
UPDATEis 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/DELETEstatements to avoid violating foreign key constraints. - Efficiency: If the transaction rolls back, the
INSERTwas 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
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
getcalls 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
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!
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
Solution: Work with the result of merge():
User managed = session.merge(detached);
managed.setName("new"); // NOW tracked
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
}
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
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
}
Summary
- Persistence Context = 1st-level cache + identity map + dirty checking + write-behind. It is the heart of the
Session. - Four states: Transient, Persistent, Detached, Removed. Transitions are managed via session methods.
-
persist()≠save(),merge()≠update(). Understand the difference. -
merge()returns a copy; the original object remains detached. - Dirty checking triggers on
flush, not onset. -
flush()does not commit; it only synchronizes. - After session closure, objects are Detached → lazy fields are inaccessible.
- The identity map lives only within a session — across sessions, objects are different instances.
- Long session = memory leak. Do not keep it open longer than necessary.
-
LazyInitializationExceptionis a symptom of improper state management, not a "Hibernate bug".
Top comments (0)