Files
hibernate-demo/src/test/java/com/ankurm/hibernatedemo/cache/QueryCacheWithoutEntityCacheTest.java
T

139 lines
6.7 KiB
Java

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.
*
* <p>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.
*
* <p>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<UncachedProduct> first;
try (Session s1 = sessionFactory.openSession()) {
Query<UncachedProduct> q = s1.createQuery(hql, UncachedProduct.class);
q.setCacheable(true);
first = q.list();
}
long queriesForFirstRun = stats.getPrepareStatementCount();
long queryCacheMissesAfterFirst = stats.getQueryCacheMissCount();
stats.clear();
List<UncachedProduct> second;
try (Session s2 = sessionFactory.openSession()) {
Query<UncachedProduct> q = s2.createQuery(hql, UncachedProduct.class);
q.setCacheable(true);
second = q.list();
}
long queriesForSecondRun = stats.getPrepareStatementCount();
long queryCacheHitsAfterSecond = stats.getQueryCacheHitCount();
long l2EntityHitsAfterSecond = stats.getSecondLevelCacheHitCount();
System.out.println("RESULT[cache-query-without-entity-cache]: first run (cold, new session) SQL statements="
+ queriesForFirstRun + ", query-cache misses=" + queryCacheMissesAfterFirst
+ " | second run (new session, query-cache HIT) SQL statements=" + queriesForSecondRun
+ ", query-cache hits=" + queryCacheHitsAfterSecond
+ ", L2 entity-cache hits=" + l2EntityHitsAfterSecond
+ " -- contrary to the original article, the second run costs ZERO SQL statements: "
+ "the query cache stores the full row tuples, not just the 5 ids, and Hibernate "
+ "rebuilds UncachedProduct instances straight from that stored data. The zero L2 "
+ "entity-cache hits confirm this does not depend on UncachedProduct having its own "
+ "L2 region at all -- it has none.");
assertThat(first).hasSize(5);
assertThat(second).hasSize(5);
assertThat(queriesForFirstRun)
.as("the first, cold run executes the query itself as ONE statement")
.isEqualTo(1);
assertThat(queryCacheHitsAfterSecond)
.as("the second run IS a genuine query-cache hit")
.isEqualTo(1L);
assertThat(queriesForSecondRun)
.as("corrected finding: the second run costs ZERO SQL statements, not the 5 individual "
+ "SELECTs the original article claimed -- the query cache reconstitutes the "
+ "entities directly from its own stored tuple data")
.isZero();
assertThat(l2EntityHitsAfterSecond)
.as("and none of that reconstruction is an L2 entity-cache hit -- UncachedProduct has "
+ "no L2 region to hit, so the mechanism is the query cache's own stored data, "
+ "not a secretly-enabled entity cache")
.isZero();
}
}