Entity Relationships & Associations

Fetch Types: EAGER vs LAZY

18 min Lesson 6 of 13

Fetch Types: EAGER vs LAZY

Every association you map with Hibernate — @OneToOne, @OneToMany, @ManyToOne, or @ManyToMany — carries a fetch strategy that answers one question: when should Hibernate load the related data? The answer has profound consequences for correctness, memory use, and query performance. Getting it wrong is one of the most common causes of slow Spring Boot applications.

The Two Strategies

EAGER fetching loads the association immediately, in the same operation that loads the owning entity. If you load a Customer and the orders collection is EAGER, Hibernate will execute whatever SQL is necessary to populate that collection right away — you never hold a Customer whose orders field is uninitialized.

LAZY fetching defers the load. Hibernate gives you a proxy (for a single association) or an uninitialised collection wrapper (for a collection). The real SQL fires only the first time your code actually touches that proxy or iterates the collection.

Hibernate's Defaults

The JPA specification defines the defaults and Hibernate obeys them:

  • @ManyToOne — defaults to EAGER
  • @OneToOne — defaults to EAGER
  • @OneToMany — defaults to LAZY
  • @ManyToMany — defaults to LAZY
The EAGER defaults for @ManyToOne and @OneToOne are a trap. They look harmless with a single entity but turn into a performance disaster once you load lists of entities. This is the root cause of the N+1 select problem you will study in the next lesson.

You override the default with the fetch attribute:

import jakarta.persistence.*; @Entity public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // Override the EAGER default — load the customer only when needed @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "customer_id") private Customer customer; // Keep the LAZY default explicit for clarity @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL) private List<OrderLine> lines = new ArrayList<>(); }

How LAZY Works in Practice

When you mark an association LAZY, Hibernate substitutes a proxy object (for @ManyToOne / @OneToOne) or an uninitialised PersistentBag / PersistentList (for collections). The proxy looks like the real type but contains only the primary key. As soon as any non-identifier getter is called, Hibernate fires the SQL and replaces the proxy with the real data.

// Inside a @Transactional service method: Order order = orderRepository.findById(1L).orElseThrow(); // At this point: order.customer is a proxy — no SQL for Customer yet System.out.println(order.getId()); // just the ID, no proxy initialisation String name = order.getCustomer().getName(); // NOW Hibernate fires: // SELECT * FROM customers WHERE id = ?
Proxy initialisation requires an open Session. Once the Hibernate Session (the unit-of-work behind a transaction) is closed, any attempt to touch an uninitialised proxy throws LazyInitializationException. This is the most frequently encountered Hibernate runtime error.

LazyInitializationException — The Classic Mistake

The error appears when you load an entity inside a service method, return it to a controller or serializer that runs outside the transaction, and then the serializer tries to traverse an uninitialized collection:

// BAD: transaction closes when findById returns; Jackson tries to serialise // order.lines after the session is gone @GetMapping("/orders/{id}") public Order getOrder(@PathVariable Long id) { return orderRepository.findById(id).orElseThrow(); // session closes here } // Jackson calls order.getLines() → LazyInitializationException

There are three correct solutions, in order of preference:

  1. Use a DTO — project only what the caller needs inside the transaction; never expose entities from controllers.
  2. Use a JOIN FETCH query — force-load exactly the associations you need for this specific use case (covered in lesson 8).
  3. Use @Transactional on the service method — keeps the session open while the data is accessed (be careful with the Open-Session-in-View anti-pattern below).

Open-Session-in-View — A Contentious Default

Spring Boot enables Open Session in View (OSIV) by default (spring.jpa.open-in-view=true). This keeps the Hibernate session open for the entire HTTP request lifecycle, including the view-rendering / JSON-serialisation phase. It masks LazyInitializationException but at a serious cost: the database connection is held for the full duration of the request, including any remote calls or slow rendering that happens after the service layer returns.

Disable OSIV in production services. Set spring.jpa.open-in-view=false in application.properties. You will get LazyInitializationExceptions at first — treat them as useful signals telling you exactly which associations need a JOIN FETCH or a DTO projection. The discipline pays off in lower connection-pool pressure and more predictable latency.

Choosing the Right Strategy

The rule of thumb used by experienced Hibernate developers:

  • Always use LAZY as your default for all associations. Explicitly fetch what you need per query.
  • EAGER at the mapping level is only appropriate for tiny, truly-always-needed value objects that will never grow (rare).
  • Fetch eagerly per query using JOIN FETCH in JPQL or Entity Graphs — not by changing the mapping.
// application.properties — good defaults for any production application spring.jpa.open-in-view=false # Hibernate SQL logging (useful during development) spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true

EAGER Can Hide Cartesian Products

When an entity has two or more EAGER collections, Hibernate may join them both in a single SQL query and produce a Cartesian product: if a Customer has 10 orders and each order has 5 lines, the join returns 50 rows for one customer. Hibernate deduplicates them in memory, but the database did 50× the work. This is often invisible during development (small data sets) and catastrophic in production.

Summary

Fetch type is the knob that controls when SQL is emitted for associated data. The JPA defaults — EAGER for @ManyToOne and @OneToOne, LAZY for collections — should be overridden so that everything defaults to LAZY. Load associations eagerly only when you explicitly need them for a specific operation, using per-query mechanisms like JOIN FETCH or Entity Graphs rather than changing the mapping. Disable Open-Session-in-View, treat LazyInitializationException as a diagnostic, and prefer DTO projections. The next two lessons translate these principles into concrete JPQL and Entity Graph patterns.