161 lines
7.8 KiB
Java
Executable File
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);
|
|
}
|
|
}
|