Stale Entity State during Paginated Query Retrieval in Hibernate L2C
0 reputation · 19 Aug 2023, 23:10 UTC
0 reputation · 19 Aug 2023, 23:10 UTC
When utilizing Hibernate ORM with a Second-Level Cache (L2C), pagination is typically managed via setFirstResult() and setMaxResults() to implement database-level LIMIT/OFFSET clauses. While the query itself identifies the correct entity IDs from the database, the resulting entities are often resolved through the L2C.
A potential synchronization gap exists when entities are modified in the database by external processes or separate sessions that do not trigger a cache invalidation for the specific query region. This can lead to a scenario where the paginated result set contains entity IDs that are current, but the entity states retrieved from the L2C are outdated.
Does Hibernate provide a mechanism to force a refresh of entities retrieved via paginated queries if they exist in the L2C but are suspected to be stale? How does the Query Cache specifically handle invalidation when only a subset of a paginated result set is modified?
Hibernate has no built-in mechanism to selectively refresh L2C entities that a paginated query happens to return. The practical options are CacheMode.IGNORE (or REFRESH) on that query, explicit eviction of the affected entities or region, or session.refresh() per entity. And the Query Cache question has a cleaner answer than you might expect: it never held your entity state in the first place.
The confusion usually comes from treating "the cache" as one thing. There are two distinct mechanisms:
So when your paginated query runs, the ID list may come from the database (or the Query Cache), but each entity is then hydrated from the L2C if present. If an external process updated the row without going through Hibernate, the ID list is current while the hydrated state is stale — exactly the symptom you describe. The fix must target the L2C, not the Query Cache.
For a paginated query where you suspect staleness, override the cache mode for that execution:
List<Order> page = session.createQuery(
"from Order o order by o.id", Order.class)
.setFirstResult(offset)
.setMaxResults(pageSize)
.setCacheMode(CacheMode.REFRESH) // read DB, overwrite L2C entries
.getResultList();
CacheMode.REFRESH bypasses the L2C on read and writes the freshly loaded state back into the cache, healing it for subsequent readers. CacheMode.IGNORE also bypasses the cache on read but does not update it — useful if you don't trust the current entries but don't want this query to repopulate them either. Neither requires a session clear.
If you already have the entities in hand and only a few are suspect, per-entity refresh is cheaper:
session.refresh(order); // forces SELECT, updates L2C
Or evict the stale entries so the next load repopulates from the database:
session.getSessionFactory().getCache()
.evict(Order.class, orderId);
The Query Cache does not track which rows appeared in which result. Invalidation is driven by update timestamps: each cached query result records which entity types (table spaces) it touches, and any Hibernate-mediated insert, update, or delete against that type bumps the timestamp for the whole space, invalidating all cached queries that reference it — not just the page containing the modified entity.
Two consequences for your scenario:
Order invalidates every cached query involving Order.Before changing cache modes, confirm where the staleness enters:
hibernate.generate_statistics=true) and watch L2C hit/miss counts alongside query executions. A paginated query with zero database entity loads but high L2C hits confirms hydration from cache.@Version value against the database. A mismatch confirms L2C staleness rather than a pagination bug.Behavior described here assumes Hibernate ORM 5.x/6.x with a standard region factory (Ehcache, Infinispan, etc.); concurrency strategies matter — nonstrict-read-write has a wider staleness window than read-write even for Hibernate-mediated writes. If external writers are a permanent part of your architecture, the durable fix is a short TTL on that region or coordinating invalidation, not per-query cache-mode overrides.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.