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:
*
*
* - {@code jakarta.persistence.loadgraph} -- attributes not in the graph keep their
* mapped fetch type. {@code publisher} stays EAGER and gets joined anyway.
* - {@code jakarta.persistence.fetchgraph} -- the graph IS the complete fetch plan.
* Any attribute not in it is forced to LAZY, so {@code publisher} does NOT get joined,
* regardless of its mapped EAGER fetch type.
*
*/
@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();
}
}