Hibernate First Level Cache: Persistence Context Explained

The Hibernate first level cache is the persistence context of one EntityManager. See with counted SQL on Hibernate 7.4 when find() and queries hit the database, how detach(), evict(), clear() and refresh() work, and how to keep long transactions small.

logo

The Hibernate first level cache is the persistence context, which is a map of entity objects that every Session or EntityManager keeps while it is open. When we load an entity by its id, Hibernate looks in this map first and runs a SELECT only when the entity is not there yet. The cache is always on and belongs to one EntityManager, so it is gone when that EntityManager closes.

We rely on the first level cache to save queries inside one unit of work, such as one web request or one transaction, but not across the application.

The following example declares a plain Movie entity and loads the same movie three times in one transaction.

@Entity
public class Movie {

  @Id
  @GeneratedValue
  private Long id;

  private String title;
  private int releaseYear;
  private double rating;
}
emf.runInTransaction(em -> {
  Movie first = em.find(Movie.class, 1L);    // select ... from Movie m1_0 where m1_0.id=?
  Movie second = em.find(Movie.class, 1L);   // no SQL, taken from the cache
  boolean same = first == second;            // same = true
  em.detach(first);                          // removes Movie#1 from the cache
  Movie third = em.find(Movie.class, 1L);    // select ... (runs again)
});

Notice that only the first and the third find() run a SELECT, because detach() removes Movie#1 from the cache between them.

Next, we see how the cache works inside the persistence context and test each case with an SQL counter. After that, we cover queries, stale data, detaching and memory growth in long sessions.

1. How the First Level Cache Works

An EntityManager is the object we use to load and save entities, and Hibernate’s Session extends it. Each EntityManager owns a persistence context, which is a set of entity instances with only one instance per database identity. For every managed entity, the context holds two things.

  • The entity object itself, stored under a key made of the entity class and the id, such as Movie#1.
  • A copy of the values loaded from the database. At flush time, Hibernate compares the object with this copy to find changes (dirty checking) and writes an UPDATE only for changed entities.

Every lookup by id goes through the context first. A hit returns the object we already have, whereas a miss runs one SELECT and stores the result for the next lookup.

Flowchart: em.find(Movie.class, 1) checks whether Movie#1 is in the persistence context; on a hit it returns the same instance without SQL; on a miss it runs select from Movie where id=?, stores the instance and a copy of its loaded values in the context, and returns it. A box shows the context holding Movie#1 Inception and Movie#2 Up, keyed by entity class and id
A cache hit costs no SQL and returns the same object. A miss runs one SELECT and fills the cache for the next call.

Not every operation reads the cache, but every operation that returns a managed entity writes to it.

OperationReads the cache first?Stores the result in the cache?
em.find(Movie.class, id)Yes, no SQL on a hitYes
em.getReference(Movie.class, id)Yes, returns the cached instanceYes
JPQL queryNo, the SQL always runsYes, and returns cached instances for rows it already has
em.persist(movie)Not a lookupYes, before the INSERT runs
Bulk update queryNoNo, cached objects keep their old values

Hibernate calls this cache “first level” because there is an optional second-level cache behind it. The second-level cache is shared by all EntityManager instances, and section 3.4 compares the two.

2. First Level Cache Example

Our example is a small movie catalog that runs on Hibernate 7.4.11, Java 25 and an in-memory H2 database. The example saves three movies and runs each case of this section, counting the SQL that reaches the database.

2.1. The Movie Entity and an SQL Counter

The Movie entity from the introduction needs no cache setting. The @GeneratedValue annotation uses a database sequence, so the three movies get the ids 1, 2 and 3.

idtitlereleaseYearrating
1Inception20108.8
2Up20098.3
3Coco20178.4

To prove a cache hit, we count statements instead of reading logs. A StatementInspector is a Hibernate callback that receives every SQL string before Hibernate prepares it. Our SqlCounter records the string and returns it unchanged.

private static final List<String> STATEMENTS = new CopyOnWriteArrayList<>();

@Override
public String inspect(String sql) {
  STATEMENTS.add(sql);
  return sql;
}

public static long selects() {
  return STATEMENTS.stream().filter(s -> s.startsWith("select")).count();
}

We register the inspector when we build the EntityManagerFactory with HibernatePersistenceConfiguration.

new HibernatePersistenceConfiguration("first-level-cache")
    .managedClasses(Movie.class)
    .jdbcUrl("jdbc:h2:mem:movies;DB_CLOSE_DELAY=-1")
    .showSql(true, false, false)
    .property(AvailableSettings.STATEMENT_INSPECTOR, new SqlCounter())
    .createEntityManagerFactory();

2.2. Loading the Same Movie Twice

Inside one transaction, the second find() call for the same id sends no SQL. It also returns the very same Java object, so == is true.

emf.runInTransaction(em -> {
  Movie first = em.find(Movie.class, 1L);
  Movie second = em.find(Movie.class, 1L);
  boolean same = first == second;           // same = true
  boolean managed = em.contains(first);     // true
});
long selects = SqlCounter.selects();        // 1
Hibernate: select m1_0.id,m1_0.rating,m1_0.releaseYear,m1_0.title from Movie m1_0 where m1_0.id=?

The identity guarantee matters beyond speed. For example, a review service and a rating service that both load Movie#1 in one transaction change one object, so one change cannot overwrite the other at flush.

2.3. Each EntityManager Has Its Own Cache

The cache belongs to one EntityManager. A second EntityManager starts with an empty context, so it runs its own SELECT and builds its own object.

try (EntityManager em1 = emf.createEntityManager();
     EntityManager em2 = emf.createEntityManager()) {
  Movie a = em1.find(Movie.class, 1L);   // select ... where m1_0.id=?
  Movie b = em2.find(Movie.class, 1L);   // select ... where m1_0.id=? (again)
  boolean same = a == b;                 // same = false
  boolean managed = em2.contains(a);     // false
}
Two EntityManagers side by side, em1 and em2, each with its own persistence context; both call find(Movie.class, 1), each runs its own SELECT against the Movie table in H2, em1 holds instance a and em2 holds instance b, and a != b
Each EntityManager fills its own cache. The same row becomes two different objects.

The same happens when one EntityManager closes and the next one starts. Two calls to emf.runInTransaction() that each load Movie#1 run two SELECT statements, because each call opens and closes its own EntityManager. In a web application, we get one cache per request or per transaction.

An EntityManager is not thread-safe, so its cache is never shared between threads. The SessionFactory (the EntityManagerFactory) is the thread-safe object we share.

2.4. persist() Adds the Entity to the Cache

A new entity enters the cache as soon as we call persist(), before the INSERT runs. A find() with its id returns the same object without any SQL.

emf.runInTransaction(em -> {
  Movie soul = new Movie("Soul", 2020, 8.0);
  em.persist(soul);                               // no SQL yet, the id comes from a preallocated sequence block
  Movie found = em.find(Movie.class, soul.getId());
  boolean same = found == soul;                   // same = true
  List<String> sent = SqlCounter.statements();    // []
});                                               // insert into Movie (rating,releaseYear,title,id) values (?,?,?,?)

2.5. Queries Always Run SQL

A JPQL query cannot use the first level cache for lookups, because Hibernate cannot know which rows match the where clause without asking the database. So the SQL runs every time, even for a movie we already loaded.

Movie found = em.find(Movie.class, 1L);                        // select ... where m1_0.id=?
Movie queried = em.createQuery("from Movie where title = :title", Movie.class)
    .setParameter("title", "Inception")
    .getSingleResult();                                         // select ... where m1_0.title=?
boolean same = queried == found;                                // same = true
// the same query again                                         // select ... where m1_0.title=?
long selects = SqlCounter.selects();                            // 3

The other direction saves SQL. A query stores every entity it returns, so a later find() for one of them runs no SQL.

List<Movie> all = em.createQuery("from Movie order by id", Movie.class)
    .getResultList();                     // select ... from Movie m1_0 order by m1_0.id
Movie up = em.find(Movie.class, 2L);      // no SQL
boolean inList = all.contains(up);        // true
long selects = SqlCounter.selects();      // 1

2.6. Stale Data and refresh()

Here the cache can surprise us. When a row changes in the database after we loaded it, for example through a bulk update or a stored procedure, the cached object keeps the old values. A query that reads the new row still returns the cached object with the old values, because Hibernate never overwrites a managed entity with query results.

Movie inception = em.find(Movie.class, 1L);                     // rating = 8.8
em.createQuery("update Movie set rating = 9.0 where id = :id")
    .setParameter("id", 1L)
    .executeUpdate();                                           // update Movie m1_0 set rating=9.0 where m1_0.id=?
double cachedRating = em.find(Movie.class, 1L).getRating();     // 8.8, no SQL
double queriedRating = em.createQuery("from Movie where id = :id", Movie.class)
    .setParameter("id", 1L)
    .getSingleResult().getRating();                             // 8.8, although the SELECT ran
em.refresh(inception);                                          // select ... where m1_0.id=?
double freshRating = inception.getRating();                     // 9.0

The method refresh() reads the row again and overwrites the object, including any change we made to it and did not flush. It works only on a managed entity; a detached one throws an IllegalArgumentException.

2.7. Removing Entities With detach(), evict() and clear()

We cannot turn the cache off, but we can take entities out of it. A removed entity becomes a detached entity, which means the object still exists in our code, but Hibernate no longer tracks it. The methods differ only in how much they remove.

MethodAPIWhat it removes
em.detach(movie)Jakarta PersistenceOne entity
session.evict(movie)Hibernate SessionOne entity, same as detach()
em.clear()BothEvery entity in the context
em.contains(movie)BothNothing; returns true when the entity is in the context

A detached movie is loaded again on the next find(), as a new object.

Session session = em.unwrap(Session.class);
Movie inception = em.find(Movie.class, 1L);             // select ...
Movie up = em.find(Movie.class, 2L);                    // select ...
int before = session.getStatistics().getEntityCount();  // 2

em.detach(inception);
boolean managed = em.contains(inception);               // false
Movie again = em.find(Movie.class, 1L);                 // select ... (third SELECT)
boolean same = again == inception;                      // same = false

session.evict(up);
boolean upManaged = em.contains(up);                    // false

em.clear();
int after = session.getStatistics().getEntityCount();   // 0

Changes we did not flush are lost when we call clear() or detach(). Hibernate writes changes at flush time, and an entity that left the context is not part of that flush. We call flush() first when the changes must be saved.

emf.runInTransaction(em -> {
  em.find(Movie.class, 2L).setTitle("Up!");
  em.clear();
});                                           // no UPDATE, the title stays "Up"

emf.runInTransaction(em -> {
  em.find(Movie.class, 2L).setTitle("Up!");
  em.flush();                                 // update Movie set rating=?,releaseYear=?,title=? where id=?
  em.clear();
});                                           // the title is now "Up!"

2.8. Memory Growth in Long Sessions

The cache never removes anything by itself while the EntityManager is open. A job that saves or reads thousands of entities in one transaction keeps every one of them, plus the copy of its loaded values, until the transaction ends. Saving 1000 movies in one transaction leaves 1000 entities in the context. Dirty checking at flush compares all of them, and a large job can run out of heap memory.

The fix is the pattern used for batch inserts, which flushes and clears the context every N entities. We use 50, the same value as hibernate.jdbc.batch_size in the example, so each flush sends the 50 inserts as one JDBC batch.

emf.runInTransaction(em -> {
  for (int i = 1; i <= 1000; i++) {
    em.persist(new Movie("Movie " + i, 2000 + i % 25, 7.0));
    if (i % 50 == 0) {
      em.flush();     // writes the 50 inserts
      em.clear();     // removes them from the cache
    }
  }
});
// most entities in the context: 1000 without clear(), 50 with clear()
Line chart of managed entities while persisting 1000 movies in one EntityManager: without clear() the count rises in a straight line to 1000; with flush() and clear() every 50 inserts it rises to 50 and drops to 0, twenty times
Without clear(), the cache grows with every entity. With flush() and clear() every 50, it never holds more than 50.

After clear(), every entity loaded before it is detached, so we keep only ids across the loop, not entity objects, and load them again with find() when needed.

The complete project on GitHub runs every case from section 2 and prints the SQL, and 24 JUnit tests check the results (mvn -q compile exec:java, mvn test).

3. First Level Cache FAQs

3.1. Can We Disable the First Level Cache in Hibernate?

No. Every Session and EntityManager has a persistence context, and there is no setting to turn it off. When we need to work without one, Hibernate offers StatelessSession, which has no persistence context and therefore no dirty checking and no cascades.

SessionFactory sf = emf.unwrap(SessionFactory.class);
try (StatelessSession stateless = sf.openStatelessSession()) {
  Movie a = stateless.get(Movie.class, 1L);   // select ... where m1_0.id=?
  Movie b = stateless.get(Movie.class, 1L);   // select ... where m1_0.id=? (again)
  boolean same = a == b;                      // same = false
}

We use it for large imports and exports, where the cache only costs memory, and we write changes with explicit insert() and update() calls.

3.2. How Do We Know Whether Hibernate Used the Cache?

Hibernate statistics count the real loads. We turn them on with hibernate.generate_statistics and compare the number of find() calls with the number of entity loads.

Statistics stats = emf.unwrap(SessionFactory.class).getStatistics();
emf.runInTransaction(em -> {
  for (int i = 0; i < 5; i++) {
    em.find(Movie.class, 1L);
  }
});
long loads = stats.getEntityLoadCount();                // 1
long statements = stats.getPrepareStatementCount();     // 1

For a single object, em.contains(movie) answers whether it is in the cache, and session.getStatistics().getEntityCount() returns how many entities the current context holds. Hibernate statistics also count query executions and collection loads.

3.3. What Replaced Session.load() in Hibernate 7?

The old Session.load(Class, id) method is gone in Hibernate 7, and session.get() is deprecated. Their Jakarta Persistence replacements read the first level cache the same way.

Old Hibernate APIReplacementBehavior
session.get(Movie.class, id)em.find(Movie.class, id)Returns the entity, runs a SELECT on a cache miss
session.load(Movie.class, id)em.getReference(Movie.class, id)Returns a proxy without SQL, or the cached entity if it is already loaded
Movie found = em.find(Movie.class, 1L);            // select ...
Movie ref = em.getReference(Movie.class, 1L);      // no SQL
boolean same = ref == found;                       // same = true

3.4. What Is the Difference Between First and Second Level Cache?

The two caches work at different scopes. The first level cache avoids repeated loads inside one unit of work, while the second-level cache avoids repeated loads across EntityManager instances and must be set up with a cache provider such as Ehcache.

First level cacheSecond level cache
ScopeOne Session or EntityManagerThe whole SessionFactory
EnabledAlways, cannot be disabledOff by default, enabled per entity with @Cache or @Cacheable
Shared between threadsNoYes
LifetimeUntil the EntityManager closes or is clearedUntil eviction or expiry in the cache provider
What it storesThe entity objects and their loaded valuesEntity data in a “dehydrated” form (a tuple of column values), not objects
Same object for two lookupsYes, == is trueNo, each EntityManager builds its own object
Remove entriesem.detach(), em.clear()emf.getCache().evict(), evictAll()

A lookup by id checks the first level cache, then the second level cache if it is enabled, and only then the database.

4. Conclusion

The first level cache is the persistence context of one EntityManager. A lookup by id returns the cached object without SQL, whereas a query always runs SQL but returns the cached objects. When an object is stale, refresh() reloads it.

We remove entities with detach() or clear() (evict() on a Session) and flush before clearing. In long transactions, we clear in batches so the context does not hold thousands of entities in memory.

5. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi ,
    What happened to the object stored in the first level cache if another session updated it in the Data Base?

    • I have the same doubt. Will the cached object gets updated?
      Also can we use merge() method to update the cached object? If so why use evict() when we can use merge() ? Any replies would be truly appreciated

      • If there is some direct DB update, session will not know it and no cache update will happen. Merge method will overwrite in the DB again. Based on whether you want to fetch latest data from database (refresh method) or overwrite database again with data stored in entity (merge method) – choose accordingly. Read more about Merging and Refreshing Entities.

        • if you want to store only update fields you can provide “dynamic-update = true” in .hbm .xml file during configuration.

          So, if session load object update A field, it will only update a field.

  2. I have few doubts regarding hibernate cache.

    1. Where the cached object is stored exactly? In which memory?

    2. Lets say I am loading an object first time using load method. It will be cached in session.
    Next time in the same session, I am trying to query same object with a hql query with some criteria. Will it be fetched from cache or database?

  3. lokesh
    first
    user 1 execure query thru hibernate
    “select * from dept where id =40?”

    then again user 2 query thru hibernate

    “select * from dept where id =50?”

    how does hibenate figure out if it needs to go to cache or go to databsae?

    in this case obsivlosuly it needs to go to DB

    • i mean
      user 1 execute query thru hibernate
      “select * from dept where id =40?”

      then again user 1 query thru hibernate

      “select * from dept where id =50?”

  4. java.lang.ClassNotFoundException: org.hibernate.cache.EhCacheProvider
    at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1714)
    at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1559)
    at java.lang.Class.forName0(Native Method)

  5. Very brief and clear explanination, thanks for the artical.

    To dig the issue (intoduced by Kartthik) further ,

    How Transactions are managed to avoide updates on stale objects in hibernate.

  6. HI Lokesh,

    I am using Hibernate second level cache. But I have some points

    1. for some entities i required cache should be created at the server startup. How I can achieve it with hibernate and ehcache.
    2. In the session.load method we required Primary key for searching. But User never know the primary key they only know the Unique key like some code of the entity. then how I will get it from session.

    I hope you understand my problem.

    Thanks.

  7. very instructive, thank you so much.
    Now I’m going to read your explanation about second level.
    I’m having many problems in an clustered production enviroment, many heavy queries.

  8. Hi Lokesh, Can you please explain. How Session cache is working internally. I am asking about internal implementation of this cache mechanism.

  9. How will session cache object get update from DB if any changes happen. Will get update automatically or explicitily.?

    • In case of single session, your application code always update this cache object so it is always updated. Hibernate takes care of synchronizing the changes with database at time of flushing the session.

      Now consider another example:

      machine 1: reads the data and returns it for editing somewhere else
      machine 2: also reads the data for modification
      machine 1: updates the data and commits.
      machine 2: tries to do an update and commit.

      What will happen when the second machine tries to commit its changes? Hibernate will see that the data has changed while machine 2 was working on the data. That is, machine 2’s update is on stale data. Hibernate can’t always merge the two changes (nor is that always desired behaviour), so it rejects the second update by throwing org.hibernate.StaleObjectStateException.

      The simplest solution, perhaps, is to add a version field using @Version on your data objects. Hibernate will automatically maintain the “version” of the data. Whenever an update takes place, the version will be changed automatically by Hibernate. Your job is the check that the version hasn’t changed between when you read the data and when you update the data. If they don’t match, you can do something to handle the problem (i.e. tell the user). There are some more sophisticated techniques for preventing concurrent updates, but this is the simplest.

  10. Thanks for your brief explanation on this.However I had a doubt on how this can be useful in real time scenario where I have 2 controllers and in each controller , it is hitting the database through opening the new session object,getting the results and immediately closing the session since it not possible to open the session until second controller results. So in those scenarios where it using the First Level Cache not hitting the database for second controller.

    • How to use First Level cache if we need to hit the database for same data from more than one place in application. for example in application I have 10 pages, 1st page, I hit the database and while retrieving the results from database by default it stored it in cache with in that session.

      Again, I need to use same data in 5th page, how can I retrieve data from cache instead of database since it will open another session

    • In My application, I have 10 pages and in 1st page I hit the database and retrieve the results directly from database and this time it used first level cache by default.

      Again I need to use same data in 5th page, how can I use from cache instead of database.

  11. Thank you very much. WIth your explanation I being able to resolve my issue, but, I used SessionFactory.openStatelessSession() to get a “fresh” entity.

  12. Hi Lokesh, I m stuck in 1 issue,
    i m goin to delete multiple object in dao like ..

    sf.getCurrentSession.delete(object1);
    sf.getCurrentSession.delete(object2);
    sf.getCurrentSession.delete(object3);

    And getting following exception :
    org.hibernate.nonuniqueobjectexception a different object with the same identifier

    Pls help in resolving the issue.

    • Can you please share the code is possible. That will be much better. Without code it’s very hard to guess.

      Basically, what hibernate is saying is that you have two objects which have the same identifier (same primary key) but they are not the same object.

  13. Example is good, but every where you copied same out put. If you use evict() 0r clear() it should display three select queries in output.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.