Home/Learn/Hibernate & JPA/FetchType — EAGER vs LAZY

FetchType — EAGER vs LAZY

Intermediate
Relationships

LAZY 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.

Java — JPA / Hibernate
@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 HERE

LazyInitializationException — 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.

Java — Spring Data JPA
// ❌ 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.

Java — Best Practice
// ✅ 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 Aria
1

What is the difference between FetchType.LAZY and FetchType.EAGER in JPA?

EasyInfosys
2

What causes a LazyInitializationException and how do you fix it?

MediumAmazon
3

Why is EAGER fetch on a @OneToMany relationship considered bad practice?

MediumThoughtworks
4

What is the difference between JOIN FETCH and FetchType.EAGER?

HardFlipkart
5

What does spring.jpa.open-in-view do and why should you disable it in production?

MediumNetflix

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.

Loading discussion…