Files
hibernate-demo/src/test/java/com/ankurm/hibernatedemo/naturalid/NaturalIdL2CacheTest.java
T

161 lines
7.8 KiB
Java
Executable File

package com.ankurm.hibernatedemo.naturalid;
import static org.assertj.core.api.Assertions.assertThat;
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.stat.Statistics;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/**
* The other half of the central empirical question: turn ON the natural-id resolution L2
* cache (@NaturalIdCache + @Cache, backed by hibernate-jcache + ehcache) and count queries
* for a bySimpleNaturalId lookup from a SECOND, brand-new session -- something
* {@link NaturalIdL1CacheTest} proved fails (re-fires a query) without L2.
*
* <p>Docs: docs/06-natural-ids.md, Topic 3.
*/
class NaturalIdL2CacheTest {
private StandardServiceRegistry registry;
private SessionFactory sessionFactory;
private void bootWithL2Cache() {
registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.driver_class", "org.h2.Driver")
.applySetting("hibernate.connection.url", "jdbc:h2:mem:naturalidl2;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.region.factory_class", "jcache")
.applySetting("hibernate.javax.cache.provider", "org.ehcache.jsr107.EhcacheCachingProvider")
.build();
Metadata metadata = new MetadataSources(registry)
.addAnnotatedClass(CachedNaturalIdProduct.class)
.buildMetadata();
sessionFactory = metadata.buildSessionFactory();
}
@AfterEach
void tearDown() {
if (sessionFactory != null) {
sessionFactory.close();
}
if (registry != null) {
StandardServiceRegistryBuilder.destroy(registry);
}
}
@Test
void bySimpleNaturalId_withNaturalIdCache_crossSession_savesTheQuery() {
bootWithL2Cache();
Statistics stats = sessionFactory.getStatistics();
stats.clear();
Long id;
try (Session seed = sessionFactory.openSession()) {
Transaction tx = seed.beginTransaction();
CachedNaturalIdProduct p = new CachedNaturalIdProduct("SKU-L2-1", "Cached Widget");
seed.persist(p);
tx.commit();
id = p.getId();
}
System.out.println("RESULT[naturalid-l2-cache]: immediately after persist()+commit(): queries="
+ stats.getPrepareStatementCount() + ", naturalId cache put=" + stats.getNaturalIdCachePutCount()
+ ", naturalId cache hit=" + stats.getNaturalIdCacheHitCount()
+ " -- @NaturalIdCache populates the L2 region on INSERT, before anyone ever looked it up.");
stats.clear();
// Session 1: lookup right after insert. Given the surprise above, this is ALREADY a
// cache hit, not a cold DB miss -- there is no query left to save at this point.
try (Session s1 = sessionFactory.openSession()) {
CachedNaturalIdProduct p = s1.bySimpleNaturalId(CachedNaturalIdProduct.class).load("SKU-L2-1");
assertThat(p.getId()).isEqualTo(id);
}
long queriesAfterSession1 = stats.getPrepareStatementCount();
// Session 2: BRAND NEW session, same natural id. With NO L2 cache (see NaturalIdL1CacheTest)
// this re-fires a query. With @NaturalIdCache + L2 enabled, it should NOT.
try (Session s2 = sessionFactory.openSession()) {
CachedNaturalIdProduct p = s2.bySimpleNaturalId(CachedNaturalIdProduct.class).load("SKU-L2-1");
assertThat(p.getId()).isEqualTo(id);
}
long queriesAfterSession2 = stats.getPrepareStatementCount();
long naturalIdCacheHitsAfterSession2 = stats.getNaturalIdCacheHitCount();
System.out.println("RESULT[naturalid-l2-cache]: session1 (post-insert) cumulative queries=" + queriesAfterSession1
+ " | session2 (new session, same natural id) cumulative queries=" + queriesAfterSession2
+ ", naturalId cache hits=" + naturalIdCacheHitsAfterSession2);
// Surprise (see the persist()/commit() log line above): @NaturalIdCache populates the L2
// region on INSERT itself, so even "session1" here is already a cache hit, not a cold
// DB miss -- there is no query left to save by the time we get here.
assertThat(queriesAfterSession1)
.as("both post-insert lookups resolve entirely from the L2 natural-id cache -- zero queries")
.isEqualTo(0);
assertThat(queriesAfterSession2)
.as("session2, a BRAND NEW session, must not fire any query either -- the L2 natural-id cache resolves it")
.isEqualTo(queriesAfterSession1);
assertThat(naturalIdCacheHitsAfterSession2)
.as("both lookups (session1 and session2) register as L2 natural-id cache hits")
.isEqualTo(2L);
}
@Test
void bySimpleNaturalId_rowInsertedOutsideHibernate_firstLookupIsARealMiss_secondIsARealHit() {
bootWithL2Cache();
Statistics stats = sessionFactory.getStatistics();
// Insert via raw JDBC, bypassing Hibernate entirely -- @NaturalIdCache never gets a
// chance to populate the L2 region at insert time, unlike the persist()-based test above.
try (Session s = sessionFactory.openSession()) {
s.doWork(connection -> {
try (var stmt = connection.createStatement()) {
stmt.execute("insert into CachedNaturalIdProduct (id, sku, name) "
+ "values (999, 'SKU-L2-RAW', 'Raw Insert Widget')");
}
connection.commit();
});
}
stats.clear();
try (Session s1 = sessionFactory.openSession()) {
CachedNaturalIdProduct p = s1.bySimpleNaturalId(CachedNaturalIdProduct.class).load("SKU-L2-RAW");
assertThat(p.getName()).isEqualTo("Raw Insert Widget");
}
long queriesAfterMiss = stats.getPrepareStatementCount();
long missesAfterFirst = stats.getNaturalIdCacheMissCount();
long putsAfterFirst = stats.getNaturalIdCachePutCount();
try (Session s2 = sessionFactory.openSession()) {
CachedNaturalIdProduct p = s2.bySimpleNaturalId(CachedNaturalIdProduct.class).load("SKU-L2-RAW");
assertThat(p.getName()).isEqualTo("Raw Insert Widget");
}
long queriesAfterHit = stats.getPrepareStatementCount();
long hitsAfterSecond = stats.getNaturalIdCacheHitCount();
System.out.println("RESULT[naturalid-l2-cache-real-miss-then-hit]: first lookup (genuine cold row) queries="
+ queriesAfterMiss + ", naturalId miss=" + missesAfterFirst + ", naturalId put=" + putsAfterFirst
+ " | second lookup (new session) cumulative queries=" + queriesAfterHit
+ ", naturalId hits=" + hitsAfterSecond);
assertThat(queriesAfterMiss).as("the first lookup of a row Hibernate never cached must hit the DB").isEqualTo(1);
assertThat(missesAfterFirst).isEqualTo(1L);
assertThat(putsAfterFirst).isEqualTo(1L);
assertThat(queriesAfterHit)
.as("the second lookup, from a brand-new session, must be satisfied entirely from the L2 cache")
.isEqualTo(queriesAfterMiss);
assertThat(hitsAfterSecond).isEqualTo(1L);
}
}