package com.ankurm.hibernatedemo.proxy; import static org.assertj.core.api.Assertions.assertThat; import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import java.util.Map; import org.hibernate.Hibernate; import org.hibernate.SessionFactory; import org.hibernate.stat.Statistics; 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 docs/11-proxies-and-lazy-initialization.md, chapter "fetchgraph vs loadgraph, actually demonstrated". * *

{@link ProxyBook} has two associations: {@code publisher} (default fetch type, EAGER) and * {@code reviews} (explicit LAZY). The named graph {@code Book.reviews-only} names only * {@code reviews}. The two JPA hint keys treat the association NOT named in the graph * differently: * *

*/ @SpringBootTest class EntityGraphFetchTest { private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); @Autowired private EntityManagerFactory emf; private Statistics stats() { return emf.unwrap(SessionFactory.class).getStatistics(); } private Long seedBookWithReviewAndPublisher() { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); ProxyPublisher publisher = new ProxyPublisher("Graph Press"); em.persist(publisher); ProxyBook book = new ProxyBook("Graph Book", publisher); em.persist(book); ProxyReview review = new ProxyReview("Nice graph", book); em.persist(review); em.getTransaction().commit(); Long id = book.getId(); em.close(); return id; } @Test void loadgraph_joinsNamedAttributeAndKeepsMappedEagerAttributeJoinedToo() { Long id = seedBookWithReviewAndPublisher(); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); EntityGraph graph = em.getEntityGraph("Book.reviews-only"); stats().clear(); ProxyBook book = em.find(ProxyBook.class, id, Map.of("jakarta.persistence.loadgraph", graph)); // One SELECT total: publisher (mapped EAGER, loadgraph does not touch it) and // reviews (named in the graph) are both fetched without a second round-trip. assertThat(stats().getPrepareStatementCount()) .as("loadgraph must fetch the named attribute AND keep the mapped-EAGER one, in one query") .isEqualTo(1); assertThat(Hibernate.isInitialized(book.getPublisher())) .as("loadgraph does not demote attributes outside the graph -- publisher is still EAGER") .isTrue(); assertThat(Hibernate.isInitialized(book.getReviews())).isTrue(); em.getTransaction().commit(); em.close(); } @Test void fetchgraph_joinsOnlyNamedAttributeAndForcesMappedEagerAttributeToLazy() { Long id = seedBookWithReviewAndPublisher(); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); EntityGraph graph = em.getEntityGraph("Book.reviews-only"); stats().clear(); ProxyBook book = em.find(ProxyBook.class, id, Map.of("jakarta.persistence.fetchgraph", graph)); assertThat(stats().getPrepareStatementCount()) .as("fetchgraph's single SELECT still includes the join for the named attribute") .isEqualTo(1); assertThat(Hibernate.isInitialized(book.getReviews())).isTrue(); assertThat(Hibernate.isInitialized(book.getPublisher())) .as("fetchgraph treats the graph as the WHOLE fetch plan -- publisher is forced back to LAZY " + "even though its mapping says EAGER") .isFalse(); DEMO.info("fetchgraph: book.getPublisher() runtime class = {}", book.getPublisher().getClass().getName()); em.getTransaction().commit(); em.close(); } @Test void noGraphAtAll_defaultFindStillJoinsTheMappedEagerAssociationButLeavesLazyCollectionAlone() { Long id = seedBookWithReviewAndPublisher(); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); stats().clear(); ProxyBook book = em.find(ProxyBook.class, id); assertThat(stats().getPrepareStatementCount()).isEqualTo(1); assertThat(Hibernate.isInitialized(book.getPublisher())) .as("plain find() honours the mapping: publisher is EAGER by default") .isTrue(); assertThat(Hibernate.isInitialized(book.getReviews())) .as("plain find() honours the mapping: reviews is LAZY by default") .isFalse(); em.getTransaction().commit(); em.close(); } }