A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 20 tests producing 22 transcripts under docs/output/, plus 12 documentation chapters. Findings the build pins: - @EnableCaching has no exposeProxy attribute; the widely-copied @EnableCaching(exposeProxy = true) does not compile. - Two methods sharing a cache name and an argument type share a key space, and one silently serves the other's answers. - The documented cache-provider detection order does not match CacheType's enum order in 4.1.1: COUCHBASE before INFINISPAN, and CACHE2K before CAFFEINE. - beforeInvocation = true is NOT deferred by TransactionAwareCacheManagerProxy on 7.0.9 - doEvict picks evictIfPresent, which the decorator does not intercept. - Four of five invalid declarations start a clean context and throw at the first call. - Caffeine on the classpath silently displaces the simple provider.
69 lines
3.8 KiB
Markdown
69 lines
3.8 KiB
Markdown
[← providers and TTL](08-providers-and-ttl.md) · [next: transactions →](10-transactions.md)
|
|
|
|
# 9. This is not the Hibernate second-level cache
|
|
|
|
They are both called caching, they both make things faster, and they solve different problems at
|
|
different layers. Mixing them up produces designs that cache the wrong thing.
|
|
|
|
## Three layers, three scopes
|
|
|
|
| | Persistence context (L1) | Hibernate second-level cache (L2) | Spring cache abstraction |
|
|
|---|---|---|---|
|
|
| Scope | one `EntityManager` / transaction | one `SessionFactory`, all sessions | one `CacheManager`, whatever you annotate |
|
|
| Unit | managed entity instance | dehydrated entity state, by id | whatever object a method returned |
|
|
| Lookup by | identity map | entity id (plus query and collection regions) | a key the `KeyGenerator` built |
|
|
| Turned on by | always on | `hibernate.cache.use_second_level_cache` + `@Cache` | `@EnableCaching` + `@Cacheable` |
|
|
| Invalidated by | end of transaction | Hibernate, on write, automatically | you, with `@CacheEvict` |
|
|
| Knows about your database | yes | yes | **no** |
|
|
|
|
Ankur's Hibernate articles cover the first two in detail:
|
|
[the first-level cache](https://ankurm.com/master-hibernate-7-first-level-cache-the-secret-to-high-performance-java-apps/),
|
|
[the second-level cache and the three ways it stales your data](https://ankurm.com/mastering-hibernate-7-second-level-cache-boosting-performance-for-modern-java-apps/),
|
|
and [configuring Ehcache 3 under it](https://ankurm.com/master-hibernate-7-ehcache-3-configuration-high-performance-caching-with-jakarta-persistence/).
|
|
|
|
## The difference that bites
|
|
|
|
Hibernate's L2 stores *dehydrated state* — a flat array of column values keyed by entity id. On a
|
|
hit it rehydrates that state into a managed entity attached to the current session. Lazy
|
|
associations still work, because the resulting entity is live.
|
|
|
|
The Spring cache abstraction stores *the object your method returned*, as-is. If that object is a
|
|
JPA entity, what you cached is a detached entity with whatever its proxies were holding at the
|
|
moment the transaction closed. `docs/output/14-cached-entity.txt`:
|
|
|
|
```
|
|
loadEntity(1) -> Alice (com.ankurm.caching.jpa.Customer)
|
|
second call returns the same instance? true
|
|
|
|
--- touching the lazy collection outside the session ---
|
|
org.hibernate.LazyInitializationException
|
|
Cannot lazily initialize collection of role 'com.ankurm.caching.jpa.Customer.orders' with key '1' (no session)
|
|
```
|
|
|
|
Worse, it is the *same instance* every time. Every caller shares one mutable entity. One of them
|
|
calls a setter, and every other caller sees it — and if someone merges it back, they merge a
|
|
version number that may be stale.
|
|
|
|
## The rule
|
|
|
|
**Cache DTOs, not entities.** Map to a record inside the transaction and cache that. It is
|
|
immutable, it serialises cleanly to a distributed cache, it has no session affinity, and it makes
|
|
the cached shape an explicit decision rather than an accident of your mapping.
|
|
|
|
If you genuinely want entity caching — read-mostly reference data, keyed by id, invalidated
|
|
automatically on write — that is precisely what Hibernate's L2 is for, and it will do it better
|
|
than `@Cacheable` because it understands the writes.
|
|
|
|
## Which one to reach for
|
|
|
|
| You want to avoid | Use |
|
|
|---|---|
|
|
| re-loading the same entity by id across requests | Hibernate L2 (`@Cache` on the entity) |
|
|
| re-running the same query that returns entities | Hibernate query cache, carefully — it needs L2 for the entities too |
|
|
| re-running an expensive computation, HTTP call or aggregation | the Spring cache abstraction |
|
|
| re-building a response DTO from several sources | the Spring cache abstraction |
|
|
|
|
The two compose. A service method cached with `@Cacheable` that internally loads entities served
|
|
from L2 is a perfectly reasonable stack — just be clear about which layer is answering, because
|
|
only one of them will notice when the row changes.
|