Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package com.ankurm.hibernatedemo.association;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.stat.Statistics;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4872, docs/12-association-mappings.md chapter "The @OneToOne lazy trap".
|
||||
*
|
||||
* <p>{@link LazyUser#getProfile()} is the non-owning ("mappedBy") side of an optional
|
||||
* {@code @OneToOne}, declared {@code FetchType.LAZY}. Without bytecode enhancement, Hibernate
|
||||
* cannot build a lazy proxy for it (it has no FK column to defer against), so it fires an
|
||||
* eager extra SELECT regardless of the annotation. {@link MiUser}/{@link MiProfile} show the
|
||||
* {@code @MapsId} fix: don't map the inverse side at all, look the child up on demand by the
|
||||
* shared primary key.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class OneToOneLazyTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@BeforeEach
|
||||
void cleanTables() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
em.createQuery("DELETE FROM LazyProfile").executeUpdate();
|
||||
em.createQuery("DELETE FROM LazyUser").executeUpdate();
|
||||
em.createQuery("DELETE FROM MiProfile").executeUpdate();
|
||||
em.createQuery("DELETE FROM MiUser").executeUpdate();
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
private Statistics stats() {
|
||||
return emf.unwrap(SessionFactory.class).getStatistics();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappedBySideOneToOne_firesExtraSelectEvenThoughItIsDeclaredLazy() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
LazyUser user = new LazyUser("alice");
|
||||
em.persist(user);
|
||||
LazyProfile profile = new LazyProfile("bio text", user);
|
||||
user.setProfile(profile);
|
||||
em.persist(profile);
|
||||
em.getTransaction().commit();
|
||||
Long id = user.getId();
|
||||
em.close();
|
||||
|
||||
EntityManager em2 = emf.createEntityManager();
|
||||
stats().clear();
|
||||
LazyUser loaded = em2.find(LazyUser.class, id);
|
||||
long queriesAfterFind = stats().getPrepareStatementCount();
|
||||
DEMO.info("LazyUser.find(): {} queries fired BEFORE touching getProfile() at all (expected 2: user + eager profile join/select)", queriesAfterFind);
|
||||
|
||||
// Note: the extra SELECT for profile already happened during find(), not here.
|
||||
String bio = loaded.getProfile() == null ? null : "loaded";
|
||||
long queriesAfterAccess = stats().getPrepareStatementCount();
|
||||
DEMO.info("after touching getProfile(): {} queries total (profile={})", queriesAfterAccess, bio);
|
||||
|
||||
assertThat(queriesAfterFind)
|
||||
.as("mappedBy @OneToOne(LAZY) still fires the profile SELECT immediately during find(), not on first access")
|
||||
.isEqualTo(2);
|
||||
assertThat(queriesAfterAccess).as("no further query needed -- it already ran eagerly").isEqualTo(2);
|
||||
em2.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsIdFix_loadingUserAloneFiresOnlyOneQuery_profileFetchedOnDemand() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
MiUser user = new MiUser("bob");
|
||||
em.persist(user);
|
||||
em.flush(); // need the generated id before building the shared-PK child
|
||||
MiProfile profile = new MiProfile("bob's bio", user);
|
||||
em.persist(profile);
|
||||
em.getTransaction().commit();
|
||||
Long id = user.getId();
|
||||
em.close();
|
||||
|
||||
EntityManager em2 = emf.createEntityManager();
|
||||
stats().clear();
|
||||
MiUser loaded = em2.find(MiUser.class, id);
|
||||
long queriesForUserOnly = stats().getPrepareStatementCount();
|
||||
DEMO.info("MiUser.find() (no mappedBy field at all): {} query", queriesForUserOnly);
|
||||
|
||||
// Fetch profile ONLY when actually needed, using the shared PK -- no proxy required.
|
||||
MiProfile profileOnDemand = em2.find(MiProfile.class, id);
|
||||
long queriesAfterExplicitProfileFetch = stats().getPrepareStatementCount();
|
||||
DEMO.info("explicit MiProfile.find() by shared PK when actually needed: {} total queries", queriesAfterExplicitProfileFetch);
|
||||
|
||||
assertThat(loaded).isNotNull();
|
||||
assertThat(queriesForUserOnly)
|
||||
.as("with @MapsId and no inverse mappedBy field, loading the user alone costs exactly 1 query")
|
||||
.isEqualTo(1);
|
||||
assertThat(profileOnDemand.getId()).isEqualTo(id);
|
||||
assertThat(queriesAfterExplicitProfileFetch)
|
||||
.as("fetching the profile only happens when the code actually asks for it")
|
||||
.isEqualTo(2);
|
||||
em2.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user