package com.ankurm.hibernatedemo.cache; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.query.Query; import org.hibernate.stat.Statistics; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; /** * Checks the original article's claim -- "Query Cache without Entity Cache causes N+1 selects" -- * by actually measuring it rather than repeating it. The claim does NOT hold: with {@link * UncachedProduct} carrying no {@code @Cacheable}/{@code @Cache} at all, a second, brand-new * session repeating the same {@code setCacheable(true)} query fires ZERO SQL statements, not five. * *
The query cache region does not store only the row ids. It stores the full hydrated tuple * state of each result row (id plus every mapped column) at the moment the query first ran. * Hibernate reconstitutes {@code UncachedProduct} instances directly from that stored tuple data * on a cache hit -- no re-query of the rows, and (confirmed below via {@code * getSecondLevelCacheHitCount()}) no dependency on the entity's own L2 region at all, because * {@link UncachedProduct} does not have one. This test does not rule out an N+1 appearing for a * query that returns associations Hibernate must still initialize per row, or for a partial-hit * scenario after individual cache entries are evicted -- only the specific, simple case the * article described (a flat entity, no associations, a repeated identical query) is measured and * corrected here. * *
Docs: docs/18-ehcache-l2-configuration.md
*/
class QueryCacheWithoutEntityCacheTest {
private StandardServiceRegistry registry;
private SessionFactory sessionFactory;
private void boot() {
registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.driver_class", "org.h2.Driver")
.applySetting("hibernate.connection.url", "jdbc:h2:mem:querycachenoentity;DB_CLOSE_DELAY=-1")
.applySetting("hibernate.connection.username", "sa")
.applySetting("hibernate.connection.password", "")
.applySetting("hibernate.hbm2ddl.auto", "create")
.applySetting("hibernate.generate_statistics", "true")
.applySetting("hibernate.cache.use_second_level_cache", "true")
.applySetting("hibernate.cache.use_query_cache", "true")
.applySetting("hibernate.cache.region.factory_class", "jcache")
.applySetting("hibernate.javax.cache.provider", "org.ehcache.jsr107.EhcacheCachingProvider")
.applySetting("hibernate.javax.cache.uri", "ehcache-chapter18.xml")
.build();
Metadata metadata = new MetadataSources(registry)
.addAnnotatedClass(UncachedProduct.class)
.buildMetadata();
sessionFactory = metadata.buildSessionFactory();
}
@AfterEach
void tearDown() {
if (sessionFactory != null) {
sessionFactory.close();
}
if (registry != null) {
StandardServiceRegistryBuilder.destroy(registry);
}
}
@Test
void cachedQuery_overUncachedEntities_repeatRunIsWorseThanNoCachingAtAll() {
boot();
Statistics stats = sessionFactory.getStatistics();
try (Session seed = sessionFactory.openSession()) {
Transaction tx = seed.beginTransaction();
for (int i = 1; i <= 5; i++) {
seed.persist(new UncachedProduct("Widget " + i));
}
tx.commit();
}
String hql = "select p from UncachedProduct p order by p.id";
stats.clear();
List