Files
hibernate-demo/src/test/java/com/ankurm/hibernatedemo/association/OneToOneLazyTest.java
T

116 lines
4.9 KiB
Java
Executable File

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();
}
}