FetchType — EAGER vs LAZY
IntermediateLAZY loads related entities on first access (default for collections); EAGER loads immediately with a JOIN (default for @ManyToOne, @OneToOne); always prefer LAZY to avoid unintended queries.
Overview
FetchType controls when Hibernate loads associated entities from the database. FetchType.LAZY defers loading until the association is accessed in code — Hibernate returns a proxy object and only fires the SELECT when you call a method on it. FetchType.EAGER forces Hibernate to load the association in the same query (or an additional query) when the owning entity is loaded. The JPA defaults are: @ManyToOne and @OneToOne default to EAGER; @OneToMany and @ManyToMany default to LAZY. In practice, you should almost always use LAZY for everything and load associations explicitly when needed using JOIN FETCH or @EntityGraph — this gives you full control over what gets loaded and prevents the "accidental query storm" that EAGER causes.
LAZY vs EAGER — What Really Happens
With LAZY, Hibernate wraps the association in a proxy object (Javassist or ByteBuddy subclass). The proxy fires a SELECT the moment any method other than getId() is called on it. This is transparent — your code treats it like a real object.
With EAGER, Hibernate immediately fetches the association when the owning entity is loaded. For @ManyToOne this adds a JOIN to the query. For @OneToMany EAGER, it fires a separate SELECT per parent — the notorious N+1 embedded directly in your mapping.
The LazyInitializationException is the most common Hibernate error. It occurs when you access a LAZY proxy after the Hibernate Session (persistence context) has been closed — for example, in a view template or a @Service called from outside a @Transactional method.
@Entity
public class Order {
@Id Long id;
String reference;
// ✅ LAZY (explicitly declared) — no query until accessed
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
// ❌ EAGER on a collection — causes N+1 on every Order query
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<OrderItem> items; // Never do this on a collection!
}
// LAZY proxy demonstration
Order order = orderRepo.findById(1L).get(); // SELECT order — customer NOT loaded yet
String ref = order.getReference(); // OK — no association access
String name = order.getCustomer().getName(); // SELECT customer — proxy fires HERELazyInitializationException — Cause & Fix
LazyInitializationException (LIE) occurs when you access a LAZY proxy outside an active Hibernate Session. The Session closes at the end of a @Transactional method, so any lazy access after that point will fail.
Wrong fix: change to EAGER — this trades LIE for performance problems. Right fix 1: load what you need inside the transaction using JOIN FETCH. Right fix 2: use @Transactional on the calling method so the Session remains open. Right fix 3: use a DTO projection that selects only the columns you need, avoiding the proxy entirely.
// ❌ LazyInitializationException — accessing proxy after session closes
@Service
public class OrderService {
// @Transactional missing!
public String getCustomerName(Long orderId) {
Order order = orderRepo.findById(orderId).get(); // session closes after this line
return order.getCustomer().getName(); // LIE: no active session!
}
}
// ✅ Fix 1: JOIN FETCH inside the transaction
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.customer WHERE o.id = :id")
Optional<Order> findByIdWithCustomer(@Param("id") Long id);
}
// ✅ Fix 2: @Transactional keeps session open
@Service
public class OrderService {
@Transactional(readOnly = true) // session open for the duration of this method
public String getCustomerName(Long orderId) {
Order order = orderRepo.findById(orderId).get();
return order.getCustomer().getName(); // proxy fires within open session — OK
}
}
// ✅ Fix 3: DTO projection — no proxy, no LIE
@Query("SELECT new com.example.dto.OrderSummary(o.id, c.name) " +
"FROM Order o JOIN o.customer c WHERE o.id = :id")
Optional<OrderSummary> findSummaryById(@Param("id") Long id);EAGER on @ManyToOne — Is It Ever Acceptable?
The JPA default for @ManyToOne is EAGER. For small, frequently accessed reference data that is almost always needed alongside the parent (e.g., a Status enum table, a Country table), EAGER is acceptable because the JOIN is cheap and always useful. However, as a general rule, always declare fetch = FetchType.LAZY explicitly on every association and add JOIN FETCH only when you actually need the data — this keeps your queries predictable and easy to reason about.
// ✅ Best practice: explicit LAZY on every association
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY) // override the JPA EAGER default
private Customer customer;
@ManyToOne(fetch = FetchType.LAZY)
private Address shippingAddress;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY) // same as default
private List<OrderItem> items;
}
// Load with items when you need them — explicit, controlled
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findWithItems(@Param("id") Long id);
// Load without items when you don't — no extra query
Optional<Order> findById(Long id);Key Points to Remember
- 1LAZY (default for collections) defers loading until the proxy method is accessed; EAGER (default for @ManyToOne) loads immediately with a JOIN.
- 2Always override the @ManyToOne EAGER default to LAZY — let JOIN FETCH or @EntityGraph control loading explicitly per query.
- 3LazyInitializationException occurs when a LAZY proxy is accessed after the Hibernate Session is closed; fix with @Transactional, JOIN FETCH, or DTO projections.
- 4Never put EAGER on a @OneToMany or @ManyToMany — it embeds an N+1 problem directly into your mapping.
- 5Prefer DTO projections (SELECT new MyDto(...)) for read-only use cases — they bypass the proxy mechanism entirely.
- 6spring.jpa.open-in-view=true (Spring Boot default) keeps the session open during the HTTP request — convenient but hides lazy loading in the view layer; set it to false in production.
Interview Questions
Sign in to ask AriaWhat is the difference between FetchType.LAZY and FetchType.EAGER in JPA?
What causes a LazyInitializationException and how do you fix it?
Why is EAGER fetch on a @OneToMany relationship considered bad practice?
What is the difference between JOIN FETCH and FetchType.EAGER?
What does spring.jpa.open-in-view do and why should you disable it in production?
Ask Aria about FetchType — EAGER vs LAZY
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.