🔸TL;DR
▪️ @EntityGraph lets you override fetch strategy per query (fetch joins… but cleaner) ✅
▪️ Great to avoid N+1 and over-fetching at the same time 🎯
▪️ Works nicely with Spring Data JPA (@EntityGraph on repository methods) 🧩

🔸 THE PROBLEM IT SOLVES
You know the story 😅
▪️ LAZY everywhere → 💥 N+1 when you iterate
▪️ EAGER everywhere → 🐘 loads too much, hurts performance
▪️ join fetch in queries → ✅ works, but queries become verbose + hard to reuse
@EntityGraph gives you a 3rd way: keep mappings sane (often LAZY), and decide fetching at query time.
🔸 QUICK CONTEXT: WHAT IS AN ENTITY GRAPH?
An Entity Graph is basically a fetch plan:
▪️ which associations should be loaded now
▪️ which ones stay lazy for later
Hibernate translates it into optimized SQL (often via joins) 🧠
🔸 EXAMPLE DOMAIN
@Entity public class Order { @Id Long id; @ManyToOne(fetch = FetchType.LAZY) Customer customer; @OneToMany(mappedBy = "order", fetch = FetchType.LAZY) List<OrderLine> lines = new ArrayList<>(); } @Entity public class OrderLine { @Id Long id; @ManyToOne(fetch = FetchType.LAZY) Product product; int quantity; }
🔸 OPTION 1: DEFINE A NAMED ENTITY GRAPH ON THE ENTITY@Entity
@Entity @NamedEntityGraph( name = "Order.withCustomerAndLines", attributeNodes = { @NamedAttributeNode("customer"), @NamedAttributeNode("lines") } ) public class Order { /* ... */ }
Then use it in a query:
EntityGraph<?> graph = em.getEntityGraph("Order.withCustomerAndLines"); Order order = em.createQuery("select o from Order o where o.id = :id", Order.class) .setParameter("id", 1L) .setHint("jakarta.persistence.fetchgraph", graph) .getSingleResult();
✅ loads customer + lines in one go (typically fewer queries)
🔸 OPTION 2: ADD SUBGRAPHS (DEEP FETCH)
You want lines.product too? Use a subgraph:
@Entity @NamedEntityGraph( name = "Order.full", attributeNodes = { @NamedAttributeNode("customer"), @NamedAttributeNode(value = "lines", subgraph = "linesGraph") }, subgraphs = { @NamedSubgraph( name = "linesGraph", attributeNodes = { @NamedAttributeNode("product") } ) } ) public class Order { /* ... */ }
Now you can fetch:
▪️ Order
▪️ Order.customer
▪️ Order.lines
▪️ Order.lines.product
…without turning everything to EAGER 😌
🔸 SPRING DATA JPA: THE NICE & SIMPLE WAY
public interface OrderRepository extends JpaRepository<Order, Long> { @EntityGraph(attributePaths = {"customer", "lines"}) Optional<Order> findById(Long id); @EntityGraph(attributePaths = {"customer", "lines", "lines.product"}) List<Order> findByCustomerId(Long customerId); }
This is often the sweet spot:
▪️ no verbose JPQL
▪️ reusable fetch plan per method
▪️ clean repository code ✅
🔸 FETCHGRAPH VS LOADGRAPH (IMPORTANT)
There are two semantics:
▪️ fetchgraph: fetch only what the graph says (others treated as LAZY)
▪️ loadgraph: fetch the graph plus anything mapped as EAGER
In JPA hints:
.setHint("jakarta.persistence.fetchgraph", graph) // strict .setHint("jakarta.persistence.loadgraph", graph) // additive
Rule of thumb 👇
▪️ If you want tight control → fetchgraph
▪️ If you want “graph + defaults” → loadgraph
🔸 WHEN TO USE IT
▪️ Read endpoints (REST/GraphQL) where DTO needs a known shape 📦
▪️ Avoiding N+1 in lists (orders → lines → products) 🧯
▪️ Use-case specific fetch (same entity, different screens) 🖥️
🔸 TAKEAWAYS
▪️ Keep relations LAZY by default, and fetch intentionally 🎯
▪️ @EntityGraph = cleaner alternative to copy-pasted join fetch 🧼
▪️ Works great in Spring Data with attributePaths ✅
▪️ Use subgraphs for deep object graphs (but stay mindful of huge joins) ⚠️
#hibernate #jpa #springdata #springboot #java #backend #performance #database #orm #softwareengineering #nplusone #cleanarchitecture
Go further with Java certification:
Java👇
Spring👇
SpringBook👇
JavaBook👇