Caching and Performance
First-level cache vs second-level cache?
| First-level (L1) | Second-level (L2) | |
|---|---|---|
| Scope | one persistence context / Session | the whole SessionFactory (all sessions) |
| Enabled | always, cannot be turned off | opt-in, needs a provider (Ehcache, Caffeine via JCache, Infinispan, Hazelcast) |
| Stores | entity objects | dehydrated entity state (not objects) |
| Good for | repeatable reads within a transaction | read-mostly reference data: categories, cities, config |
How do you enable the second-level cache in Spring Boot?
spring:
jpa:
properties:
hibernate.cache.use_second_level_cache: true
hibernate.cache.region.factory_class: jcache
hibernate.javax.cache.provider: org.ehcache.jsr107.EhcacheCachingProvider@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Category { ... }Dependencies: org.hibernate.orm:hibernate-jcache plus Ehcache 3 (jakarta classifier). Strategies: READ_ONLY (never changes), NONSTRICT_READ_WRITE, READ_WRITE (soft locks), TRANSACTIONAL.
What is the query cache and when should you avoid it?
It caches the ids returned by a query for given parameters (the entities themselves come from the L2 cache). Enable with hibernate.cache.use_query_cache=true and mark queries cacheable (@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))). Any write to a table involved invalidates all cached queries on it, so it only helps for data that rarely changes.
How do you make bulk inserts fast with Hibernate?
- Enable JDBC batching:
hibernate.jdbc.batch_size=50,hibernate.order_inserts=true,hibernate.order_updates=true. - Use
SEQUENCEids (with an allocation size), notIDENTITY, which disables insert batching. - Flush and clear the persistence context every batch to keep memory flat.
- For really large loads, consider plain JDBC
batchUpdate,COPY/LOAD DATA, or a stateless session.
@Transactional
public void importProducts(List<Product> products) {
for (int i = 0; i < products.size(); i++) {
em.persist(products.get(i));
if ((i + 1) % 50 == 0) { em.flush(); em.clear(); }
}
}How do you find which SQL Hibernate runs and how many queries a request makes?
- Log SQL:
logging.level.org.hibernate.SQL=debugand bind values withlogging.level.org.hibernate.orm.jdbc.bind=trace(prefer these toshow-sql, which prints to stdout). hibernate.generate_statistics=true: query counts, cache hits, slow queries (hibernate.log_slow_query).- datasource-proxy / p6spy to log real JDBC statements and timings; assert query counts in tests.
- In production: APM (New Relic, Datadog) or the database's slow query log, then
EXPLAINand add indexes.
Bulk JPQL update vs loading and modifying entities?
Loading 10,000 entities to change one field runs 1 SELECT + 10,000 UPDATEs and fills memory. A bulk update Product p set p.price = ... where ... runs one statement. Trade-offs: bulk statements skip the persistence context (stale managed objects), lifecycle callbacks, @Version increments (unless you write update versioned in HQL or set the version yourself) and cascades.
What is @DynamicUpdate?
By default Hibernate's UPDATE sets all columns (one cached SQL string per entity, good for batching). @org.hibernate.annotations.DynamicUpdate generates an UPDATE with only the changed columns at runtime. Useful for wide tables or to reduce conflicts on concurrently updated columns; costs SQL generation each time.