Hibernate Caching
Master Hibernate caching with Mermaid diagrams covering first-level cache, second-level cache, query cache, and performance optimization strategies
Hibernate Caching is a powerful mechanism to improve application performance by reducing database queries. Understanding the different cache levels and their behavior is crucial for building high-performance applications.
Hibernate Cache Levels
graph TB
A[Hibernate Caching] --> B[First-Level Cache]
A --> C[Second-Level Cache]
A --> D[Query Cache]
B --> B1[Session-scoped]
B --> B2[Always enabled]
B --> B3[Transaction-level]
C --> C1[SessionFactory-scoped]
C --> C2[Optional]
C --> C3[Application-level]
D --> D1[Query results]
D --> D2[Requires 2nd level]
D --> D3[Explicit enable]
style A fill:#2196F3
style B fill:#4CAF50
style C fill:#FF9800
style D fill:#9C27B0
Key Points:
- First-Level Cache: Session-scoped, mandatory, transaction-level caching
- Second-Level Cache: SessionFactory-scoped, optional, application-level caching
- Query Cache: Caches query results, requires second-level cache
- Performance: Reduces database round trips significantly
- Configuration: First-level automatic, others need explicit configuration
First-Level Cache Architecture
sequenceDiagram
participant App as Application
participant Session as Hibernate Session
participant Cache as First-Level Cache
participant DB as Database
App->>Session: load(Entity.class, id)
Session->>Cache: Check cache
alt Cache Hit
Cache->>Session: Return cached entity
Session->>App: Return entity
else Cache Miss
Session->>DB: SELECT query
DB->>Session: Return data
Session->>Cache: Store in cache
Session->>App: Return entity
end
Note over Cache: Cache cleared on session.close()
Key Points:
- Automatic: Always enabled, cannot be disabled
- Scope: Lives within a single Session/Transaction
- Lifecycle: Cleared when session is closed or cleared
- Purpose: Reduces queries within same transaction
- Flush: Synchronizes cache with database before query execution
First-Level Cache Code Example
// First-level cache demonstration
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
// First load - hits database
Employee emp1 = session.get(Employee.class, 1L);
System.out.println("First load: " + emp1.getName());
// Second load - hits first-level cache (no DB query)
Employee emp2 = session.get(Employee.class, 1L);
System.out.println("Second load: " + emp2.getName());
// Both references point to same object
System.out.println(emp1 == emp2); // true
// Modify entity
emp1.setSalary(75000);
// Changes not yet in database
// flush() synchronizes cache with DB
session.flush();
// Commit writes to database
tx.commit();
session.close();
// Output:
// Hibernate: select employee0_.id, employee0_.name, employee0_.salary from Employee employee0_ where employee0_.id=?
// First load: John Doe
// Second load: John Doe
// true
// Hibernate: update Employee set name=?, salary=? where id=?
When to Explicitly Call flush()
// Use case 1: Get generated ID immediately
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
Employee newEmp = new Employee("Jane Smith", 60000);
session.save(newEmp);
// Flush to get auto-generated ID
session.flush();
Long generatedId = newEmp.getId(); // Now available
System.out.println("Generated ID: " + generatedId);
tx.commit();
session.close();
// Use case 2: Batch processing to conserve memory
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for (int i = 0; i < 10000; i++) {
Employee emp = new Employee("Employee " + i, 50000);
session.save(emp);
// Flush and clear every 50 records
if (i % 50 == 0) {
session.flush(); // Write to DB
session.clear(); // Clear cache to free memory
}
}
tx.commit();
session.close();
Second-Level Cache Architecture
graph TB
A[Application] --> B[Session 1]
A --> C[Session 2]
A --> D[Session 3]
B --> E[First-Level Cache]
C --> F[First-Level Cache]
D --> G[First-Level Cache]
E --> H[Second-Level Cache]
F --> H
G --> H
H --> I[SessionFactory]
I --> J[Database]
style H fill:#FF9800
style I fill:#2196F3
style J fill:#4CAF50
Key Points:
- Shared: Accessible across all sessions in application
- SessionFactory-scoped: Lives as long as SessionFactory
- Optional: Must be explicitly configured
- Provider: EhCache, Infinispan, Hazelcast, Redis
- Granular: Can cache specific entities, collections
Second-Level Cache Configuration
// hibernate.cfg.xml configuration
<hibernate-configuration>
<session-factory>
<!-- Enable second-level cache -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- Cache provider (EhCache) -->
<property name="hibernate.cache.region.factory_class">
org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>
<!-- Enable query cache -->
<property name="hibernate.cache.use_query_cache">true</property>
<!-- Show cache statistics -->
<property name="hibernate.generate_statistics">true</property>
</session-factory>
</hibernate-configuration>
<!-- ehcache.xml configuration -->
<ehcache>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
<!-- Cache for Employee entity -->
<cache name="com.example.Employee"
maxElementsInMemory="500"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"/>
<!-- Cache for Department entity -->
<cache name="com.example.Department"
maxElementsInMemory="100"
eternal="true"
overflowToDisk="false"/>
</ehcache>
Entity-Level Cache Configuration
// Enable caching on entity with annotations
@Entity
@Table(name = "employees")
@Cacheable
@org.hibernate.annotations.Cache(
usage = CacheConcurrencyStrategy.READ_WRITE
)
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double salary;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
// Getters and setters
}
// Or using XML mapping
<class name="com.example.Employee" table="employees">
<cache usage="read-write"/>
<id name="id" column="id">
<generator class="identity"/>
</id>
<property name="name" column="name"/>
<property name="salary" column="salary"/>
</class>
Cache Concurrency Strategies
graph LR
A[Cache Strategies] --> B[READ_ONLY]
A --> C[READ_WRITE]
A --> D[NONSTRICT_READ_WRITE]
A --> E[TRANSACTIONAL]
B --> B1[Never updated]
B --> B2[Best performance]
C --> C1[Read and write]
C --> C2[Soft locks]
D --> D1[Occasional updates]
D --> D2[No guarantees]
E --> E1[JTA only]
E --> E2[Full ACID]
style A fill:#2196F3
style B fill:#4CAF50
style C fill:#FF9800
style D fill:#9C27B0
style E fill:#F44336
Key Points:
- READ_ONLY: For immutable data, best performance
- READ_WRITE: For frequently updated data, uses soft locks
- NONSTRICT_READ_WRITE: For occasionally updated data
- TRANSACTIONAL: Full transactional support, JTA required
- Choice: Based on data update frequency and consistency needs
Second-Level Cache Code Example
// Demonstrating second-level cache
SessionFactory sessionFactory = // ... initialized
// Session 1: Load entity (cache miss, hits DB)
Session session1 = sessionFactory.openSession();
Employee emp1 = session1.get(Employee.class, 1L);
System.out.println("Session 1: " + emp1.getName());
session1.close();
// Hibernate: select ... from Employee where id=?
// Session 2: Load same entity (cache hit, no DB query)
Session session2 = sessionFactory.openSession();
Employee emp2 = session2.get(Employee.class, 1L);
System.out.println("Session 2: " + emp2.getName());
session2.close();
// No SQL query - retrieved from second-level cache
// Session 3: Update entity
Session session3 = sessionFactory.openSession();
Transaction tx = session3.beginTransaction();
Employee emp3 = session3.get(Employee.class, 1L);
emp3.setSalary(80000);
tx.commit();
session3.close();
// Cache is invalidated/updated
// Session 4: Load entity (cache hit with updated data)
Session session4 = sessionFactory.openSession();
Employee emp4 = session4.get(Employee.class, 1L);
System.out.println("Session 4 salary: " + emp4.getSalary()); // 80000
session4.close();
Query Cache
sequenceDiagram
participant App as Application
participant Session as Session
participant QCache as Query Cache
participant 2ndCache as 2nd Level Cache
participant DB as Database
App->>Session: createQuery().setCacheable(true)
Session->>QCache: Check query cache
alt Query Cache Hit
QCache->>2ndCache: Get entity IDs
2ndCache->>Session: Return entities
Session->>App: Return results
else Query Cache Miss
Session->>DB: Execute query
DB->>Session: Return results
Session->>QCache: Cache query results (IDs)
Session->>2ndCache: Cache entities
Session->>App: Return results
end
Key Points:
- Query Results: Caches query result sets (entity IDs)
- Dependency: Requires second-level cache enabled
- Explicit: Must call setCacheable(true) on query
- Invalidation: Invalidated when table data changes
- Use Case: Frequently executed queries with same parameters
Query Cache Code Example
// Enable query cache in configuration
// hibernate.cache.use_query_cache=true
Session session = sessionFactory.openSession();
// Query 1: Cache miss, hits database
Query<Employee> query1 = session.createQuery(
"FROM Employee WHERE salary > :minSalary", Employee.class);
query1.setParameter("minSalary", 50000.0);
query1.setCacheable(true); // Enable caching for this query
List<Employee> employees1 = query1.list();
System.out.println("Query 1 results: " + employees1.size());
// Query 2: Same query, cache hit (no DB query)
Query<Employee> query2 = session.createQuery(
"FROM Employee WHERE salary > :minSalary", Employee.class);
query2.setParameter("minSalary", 50000.0);
query2.setCacheable(true);
List<Employee> employees2 = query2.list();
System.out.println("Query 2 results: " + employees2.size());
session.close();
// Output:
// Hibernate: select ... from Employee where salary>?
// Query 1 results: 15
// Query 2 results: 15 (no SQL query)
// Named query with cache
@NamedQuery(
name = "Employee.findBySalaryRange",
query = "FROM Employee WHERE salary BETWEEN :min AND :max",
hints = @QueryHint(name = "org.hibernate.cacheable", value = "true")
)
public class Employee {
// ...
}
// Using named query
List<Employee> employees = session
.createNamedQuery("Employee.findBySalaryRange", Employee.class)
.setParameter("min", 40000.0)
.setParameter("max", 80000.0)
.list();
Cache Performance Monitoring
// Enable statistics
Configuration config = new Configuration();
config.setProperty("hibernate.generate_statistics", "true");
// Get statistics
SessionFactory sessionFactory = config.buildSessionFactory();
Statistics stats = sessionFactory.getStatistics();
// Perform operations
Session session = sessionFactory.openSession();
// ... perform database operations
session.close();
// Print cache statistics
System.out.println("Second Level Cache Hits: " +
stats.getSecondLevelCacheHitCount());
System.out.println("Second Level Cache Misses: " +
stats.getSecondLevelCacheMissCount());
System.out.println("Second Level Cache Puts: " +
stats.getSecondLevelCachePutCount());
System.out.println("Query Cache Hits: " +
stats.getQueryCacheHitCount());
System.out.println("Query Cache Misses: " +
stats.getQueryCacheMissCount());
System.out.println("Query Execution Count: " +
stats.getQueryExecutionCount());
// Cache hit ratio
double hitRatio = (double) stats.getSecondLevelCacheHitCount() /
(stats.getSecondLevelCacheHitCount() + stats.getSecondLevelCacheMissCount());
System.out.println("Cache Hit Ratio: " + (hitRatio * 100) + "%");
Cache Eviction and Management
// Manual cache eviction
SessionFactory sessionFactory = // ... initialized
// Evict specific entity from cache
sessionFactory.getCache().evict(Employee.class, 1L);
// Evict all instances of an entity
sessionFactory.getCache().evict(Employee.class);
// Evict entire second-level cache
sessionFactory.getCache().evictAllRegions();
// Check if entity is in cache
boolean inCache = sessionFactory.getCache()
.contains(Employee.class, 1L);
// Evict query cache
sessionFactory.getCache().evictQueryRegions();
// Evict specific query cache region
sessionFactory.getCache().evictQueryRegion("myQueryRegion");
Best Practices
- Use Second-Level Cache for Read-Heavy Data: Entities that are frequently read but rarely updated
- Choose Appropriate Strategy: READ_ONLY for immutable, READ_WRITE for mutable data
- Monitor Cache Statistics: Track hit/miss ratios to optimize cache configuration
- Set Appropriate TTL: Configure time-to-live based on data staleness tolerance
- Avoid Caching Large Objects: Can cause OutOfMemoryError
- Use Query Parameters: Don't cache entire objects in query parameters
- Enable Batch Processing: Use flush() and clear() for large batch operations
- Test Cache Behavior: Verify cache hits/misses in development
// Good practice: Use IDs in queries
String hql = "FROM Order WHERE customer.id = :customerId";
Query<Order> query = session.createQuery(hql, Order.class);
query.setParameter("customerId", 123L);
query.setCacheable(true);
// Bad practice: Using entire object
// String hql = "FROM Order WHERE customer = :customer";
// query.setParameter("customer", customerObject); // Caches entire object
Common Pitfalls
- Stale Data: Cache not invalidated when data updated outside Hibernate
- Memory Issues: Over-caching can cause OutOfMemoryError
- Cache Coherence: Multiple application instances need distributed cache
- Query Cache Invalidation: Entire cache invalidated on any table update
- Association Caching: Requires fetch="select" for second-level cache to work