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. * *
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); } }