First-Level Cache
The Persistence Context Is a Cache
The persistence context (see the basic level) keeps a map of entity id → managed object. When you call find() twice for the same id in one transaction, the second call returns the same Java object and no SQL is sent. This map is called the identity map or first-level cache.
- It cannot be switched off.
- It lives only as long as the
EntityManager(in Spring: one transaction, or one request with open-session-in-view). - It guarantees one object per row inside a session, so
==works for entities loaded in the same session.
Identity Map Demo
Example
A tiny model of what Hibernate does inside Session.find(). Each session has its own map; only a miss goes to the "database":
import java.util.*;
public class Main {
static int selects = 0;
static final Map<Long, String> DB = Map.of(1L, "iPhone 15", 2L, "boAt Earbuds");
public static void main(String[] args) {
Session s1 = new Session("session-1");
Product a = s1.find(1L);
Product b = s1.find(1L);
System.out.println("same object in one session: " + (a == b));
Session s2 = new Session("session-2");
Product c = s2.find(1L);
System.out.println("same object across sessions: " + (a == c));
System.out.println("SELECTs sent to the database: " + selects);
}
}
class Product {
final Long id;
String name;
Product(Long id, String name) { this.id = id; this.name = name; }
}
class Session {
private final String name;
private final Map<Long, Product> firstLevelCache = new HashMap<>();
Session(String name) { this.name = name; }
Product find(Long id) {
Product cached = firstLevelCache.get(id);
if (cached != null) {
System.out.println(name + ": L1 hit for Product#" + id + " (no SQL)");
return cached;
}
Main.selects++;
System.out.println(name + ": select ... from products where id=" + id);
Product loaded = new Product(id, Main.DB.get(id));
firstLevelCache.put(id, loaded);
return loaded;
}
}Output
session-1: select ... from products where id=1 session-1: L1 hit for Product#1 (no SQL) same object in one session: true session-2: select ... from products where id=1 same object across sessions: false SELECTs sent to the database: 2
What Bypasses the First-Level Cache
JPQL and native queries always go to the database. Hibernate then checks each returned row against the identity map: if the entity is already managed, it reuses the existing object and ignores the fresh column values.
@Transactional
public void showStaleRead(Long id) {
Product p1 = em.find(Product.class, id); // SELECT, stock = 10
jdbcTemplate.update("update products set stock = 0 where id = ?", id); // changed behind Hibernate's back
Product p2 = em.createQuery("select p from Product p where p.id = :id", Product.class)
.setParameter("id", id).getSingleResult(); // SELECT runs again...
System.out.println(p1 == p2); // true — same managed object
System.out.println(p2.getStock()); // 10 — old value kept!
em.refresh(p2); // forces a reload from the database
System.out.println(p2.getStock()); // 0
}Tip: Use em.refresh(entity) to re-read a row, em.detach(entity) to drop one object, and em.clear() to empty the whole first-level cache (important in batch jobs, see Performance Tuning).
Exercise
Exercise: Inside one @Transactional method you call productRepository.findById(5L) three times and then productRepository.findAll() once. How many SQL statements are sent?
Show answer
Two.
- findById(5L) the first time: 1 SELECT; the next two calls hit the first-level cache.
- findAll() is a query: it always runs SQL (1 SELECT). Product 5 in the result is
matched to the already-managed object, so the same instance is returned.