139 lines
6.7 KiB
Java
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();
|
|
}
|
|
}
|