Sessions & Caching
The Session/EntityManager, first-level vs second-level cache, and the transient/persistent/detached entity lifecycle.
The Session / EntityManager
Hibernate's native API calls it a Session; the JPA-standard equivalent (implemented by Hibernate underneath) is EntityManager. Both represent the same thing: a single unit of work against the database, holding a persistence context — an in-memory registry of every entity currently being tracked.
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
User user = entityManager.find(User.class, 1L); // loaded and now tracked by the persistence context
user.setFullName("Ada Lovelace-Byron"); // no explicit save/update call needed
entityManager.getTransaction().commit(); // Hibernate detects the change and issues an UPDATE automatically
Notice there's no explicit update() call — this is dirty checking. Because the persistence context tracks the entity's original loaded state, on commit Hibernate compares the entity's current field values against what it originally loaded, and generates an UPDATE only for the fields that actually changed.
Entity states: transient, persistent, detached
Every entity instance is, at any moment, in exactly one of these states:
new User(...) entityManager.persist(user) entityManager.close() / detach()
│ │ │
v v v
TRANSIENT ─────────────────> PERSISTENT ─────────────────────> DETACHED
(plain object, (tracked by the (was tracked,
no DB row, persistence context, but the context
not tracked) changes auto-flushed) is gone now)
| State | Meaning |
|---|---|
| Transient | A plain new User(...) — Hibernate has never seen it, no row exists, no tracking happens |
| Persistent | Loaded via find(), or after persist() — tracked by the current persistence context; field changes are automatically detected and flushed as SQL |
| Detached | Was persistent, but its persistence context has since closed (or it was explicitly detach()ed) — the object still holds its data, but changes to it are no longer tracked or auto-saved |
User user;
try (EntityManager em = emf.createEntityManager()) {
em.getTransaction().begin();
user = em.find(User.class, 1L); // persistent
em.getTransaction().commit();
} // em closes here — user becomes detached
user.setFullName("Changed after close"); // has NO effect on the database — nothing is tracking this object anymore
To persist further changes to a detached entity, it must be reattached with merge(), which returns a new, managed copy:
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
User managed = em2.merge(user); // returns a persistent copy with the new fullName
em2.getTransaction().commit(); // NOW the update is actually flushed
First-level cache (session-scoped)
The first-level cache is the persistence context itself — mandatory, always on, and scoped to a single Session/EntityManager. Requesting the same entity by ID twice within the same session returns the same object instance without a second database round-trip:
User first = entityManager.find(User.class, 1L); // hits the database
User second = entityManager.find(User.class, 1L); // returns the SAME object from the first-level cache — no query
This is why the first-level cache can't be disabled — it's inherent to what a persistence context is: a registry of everything that's been loaded or saved in this unit of work.
Second-level cache (optional, shared across sessions)
The second-level cache is an optional, separately configured cache (backed by a provider such as Ehcache or Caffeine) that's shared across sessions, letting one session benefit from data another session already loaded:
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
// rarely-changing reference data is a good candidate
}
| First-level | Second-level | |
|---|---|---|
| Scope | One Session/EntityManager |
Shared across sessions/the whole application |
| Enabled by default? | Yes, always | No — opt-in, requires a cache provider |
| Best for | Every entity, automatically | Read-heavy, rarely-changing data (reference/lookup tables) |
| Risk | None — it's just normal unit-of-work behavior | Stale data if updated outside the cached session, or misused for frequently-changing entities |
Common mistakes
- Modifying a detached entity and expecting the change to reach the database without
merge()— detached objects are inert as far as persistence is concerned. - Enabling the second-level cache for frequently-updated entities, where the cost of keeping the cache correctly invalidated outweighs the benefit — it's best suited to rarely-changing, read-heavy data.
- Assuming the first-level cache is something you configure — it's inherent to the persistence context and can't meaningfully be turned off.
- Holding a reference to an entity across a much longer scope than its originating session/transaction, then being surprised that changes silently don't persist.