Fetch Types: Lazy vs Eager
Learn how FetchType.LAZY and FetchType.EAGER control when Hibernate loads related entities, the default per relationship, and what causes LazyInitializationException.
Introduction
When you load a Department, should Hibernate also load every one of its Employees immediately, even if you never look at them? Or should it wait until you actually access the employees collection? This decision is controlled by fetch type, and getting it wrong is one of the most common sources of both performance problems and runtime exceptions in Hibernate applications.
- The difference between FetchType.LAZY and FetchType.EAGER
- The default fetch type for each relationship annotation
- What a lazy proxy is and how lazy loading actually works
- What causes LazyInitializationException and how to avoid it
Lazy vs Eager Loading
FetchType.EAGER loads the related entity or collection immediately, as part of the same query (or an immediate follow-up query) that loads the owning entity. FetchType.LAZY defers loading until the related data is actually accessed in your code, using a proxy object in the meantime.
| Fetch Type | When Data Loads | Risk |
|---|---|---|
| EAGER | Immediately, with the owning entity | Can load far more data than needed, hurting performance |
| LAZY | On first access to the field/collection | Throws LazyInitializationException if accessed after the Session is closed |
Default Fetch Types
Each relationship annotation ships with its own default, and these defaults are worth memorizing since they surprise almost everyone at least once.
| Annotation | Default Fetch Type |
|---|---|
| @OneToOne | EAGER |
| @ManyToOne | EAGER |
| @OneToMany | LAZY |
| @ManyToMany | LAZY |
Single-valued associations (@OneToOne, @ManyToOne) default to EAGER because they load at most one extra row. Collection-valued associations (@OneToMany, @ManyToMany) default to LAZY because they could load an unbounded number of rows.
@ManyToOne(fetch = FetchType.LAZY) // overriding the EAGER default@JoinColumn(name = "department_id")private Department department;
@OneToMany(mappedBy = "department", fetch = FetchType.EAGER) // overriding the LAZY defaultprivate List<Employee> employees = new ArrayList<>();Seeing Lazy Loading in Action
With the default LAZY setting on @OneToMany, loading a Department issues only one SELECT. A second SELECT fires only when the employees collection is actually iterated.
Session session = sessionFactory.openSession();
Department department = session.get(Department.class, 1L);System.out.println("Loaded department: " + department.getName());// No employees query yet — employees is still an uninitialized proxy collection
List<Employee> employees = department.getEmployees();System.out.println("First employee: " + employees.get(0).getName());// Accessing the collection triggers the query now
session.close();Click Run to see what this code prints.
LazyInitializationException
A lazy collection or reference can only be initialized while its Session is still open. If you close the Session first and then try to access the lazy field, Hibernate throws LazyInitializationException, because there is no active connection left to run the deferred query.
Department department;Session session = sessionFactory.openSession();department = session.get(Department.class, 1L);session.close(); // Session is now closed
// This throws LazyInitializationException — the Session is goneSystem.out.println(department.getEmployees().size());Click Run to see what this code prints.
This exception is especially common in web applications where an entity is loaded in a service layer, the Session closes when the transaction ends, and only later does a view/template layer try to access a lazy collection.
Fetching Eagerly with HQL
Rather than changing an annotation's default fetch type globally, you can request eager loading for a specific query using JOIN FETCH, which loads the association in the very same SELECT.
Session session = sessionFactory.openSession();
Query<Department> query = session.createQuery( "FROM Department d JOIN FETCH d.employees WHERE d.id = :id", Department.class);query.setParameter("id", 1L);
Department department = query.uniqueResult();System.out.println(department.getEmployees().size()); // safe — already loaded
session.close();Click Run to see what this code prints.
Common Mistakes
- Changing every relationship to FetchType.EAGER to "fix" LazyInitializationException, which silently kills performance across the whole application.
- Accessing a lazy field or collection after the Session that loaded it has already closed.
- Not realizing @ManyToOne and @OneToOne default to EAGER, causing unexpected extra queries or over-fetching on simple lookups.
- Using JOIN FETCH with multiple collections in a single query, which can trigger a Cartesian product and duplicate rows.
Best Practices
- Default to LAZY for every relationship and fetch eagerly only where a specific query actually needs the data, using JOIN FETCH.
- Never access lazy associations after the Session has closed — fetch what you need before closing it.
- Explicitly override @ManyToOne/@OneToOne to LAZY when the related entity is large or rarely needed.
- Watch for the N+1 query problem, where a LAZY collection triggers one extra query per parent row in a loop — covered in a later lesson.
Frequently Asked Questions
Historically, JPA set this default because a @ManyToOne reference is a single row, not a potentially large collection, so eager loading seemed low-risk. In practice, most teams override it to LAZY intentionally for consistency and control.
No — LAZY only defers loading. The moment your code accesses the field or iterates the collection, Hibernate loads it, as long as the Session is still open at that point.
Access the lazy data while the Session is still open — for example within the same service method or transaction — rather than trying to access it later. JOIN FETCH is the cleanest way to guarantee that for a specific query.
Key Takeaways
- FetchType.EAGER loads related data immediately; FetchType.LAZY defers it until access.
- @OneToOne and @ManyToOne default to EAGER; @OneToMany and @ManyToMany default to LAZY.
- LazyInitializationException happens when lazy data is accessed after the Session has closed.
- JOIN FETCH lets you eagerly load an association for one specific query without changing its default.
Summary
Fetch types are one of the most consequential settings in Hibernate — they control both correctness (avoiding LazyInitializationException) and performance (avoiding over-fetching). Next, you will learn about the first-level cache, the Session-scoped cache that already sits quietly behind every get() call you have written so far.