Caching and Performance

First-level cache vs second-level cache?

First-level (L1)Second-level (L2)
Scopeone persistence context / Sessionthe whole SessionFactory (all sessions)
Enabledalways, cannot be turned offopt-in, needs a provider (Ehcache, Caffeine via JCache, Infinispan, Hazelcast)
Storesentity objectsdehydrated entity state (not objects)
Good forrepeatable reads within a transactionread-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 SEQUENCE ids (with an allocation size), not IDENTITY, 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=debug and bind values with logging.level.org.hibernate.orm.jdbc.bind=trace (prefer these to show-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 EXPLAIN and 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.