Lazy vs Eager Fetching in JPA

Illustration for Lazy vs Eager Fetching in JPA
By Last updated:

Fetching strategy in JPA determines when related entities are loaded from the database. Choosing between Lazy and Eager fetching has a direct impact on application performance, memory usage, and query efficiency.

  • Lazy Fetching: Related entities are loaded only when accessed.
  • Eager Fetching: Related entities are loaded immediately with the parent entity.

Think of it like ordering food:

  • Lazy = You order when you’re hungry (on-demand).
  • Eager = You order everything upfront, whether you need it or not.

In this tutorial, we’ll cover Lazy vs Eager fetching in JPA, with code examples, SQL outputs, pitfalls, and best practices.


Setup and Configuration

persistence.xml

<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
    <persistence-unit name="my-pu" transaction-type="RESOURCE_LOCAL">
        <class>com.example.Department</class>
        <class>com.example.Employee</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:testdb"/>
            <property name="jakarta.persistence.jdbc.user" value="sa"/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Entity Example

import jakarta.persistence.*;
import java.util.List;

@Entity
@Table(name = "departments")
public class Department {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "department", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Employee> employees;

    // Getters and setters
}

@Entity
@Table(name = "employees")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "department_id")
    private Department department;

    // Getters and setters
}

Lazy Fetching

Example

Department dept = em.find(Department.class, 1L);
System.out.println(dept.getName()); // Employees not loaded yet

// Employees loaded only when accessed
for (Employee e : dept.getEmployees()) {
    System.out.println(e.getName());
}

SQL Output:

select d.id, d.name from departments d where d.id=1;
select e.id, e.name, e.department_id from employees e where e.department_id=1;

Eager Fetching

Example

Employee emp = em.find(Employee.class, 1L);
System.out.println(emp.getName()); // Department is loaded immediately
System.out.println(emp.getDepartment().getName());

SQL Output:

select e.id, e.name, e.department_id, d.id, d.name 
from employees e 
left join departments d on e.department_id=d.id 
where e.id=1;

CRUD with Fetching Strategies

Create

tx.begin();
Department dept = new Department();
dept.setName("IT");

Employee e1 = new Employee();
e1.setName("Alice");
e1.setDepartment(dept);

dept.setEmployees(List.of(e1));
em.persist(dept);
tx.commit();

Read (Lazy vs Eager)

  • Lazy: Employees loaded only when accessed.
  • Eager: Department loaded immediately with Employee.

Update

tx.begin();
e1.setName("Alice Updated");
em.merge(e1);
tx.commit();

Delete

tx.begin();
em.remove(dept);
tx.commit();

Persistence Context and Fetching

  • Persistence Context ensures one entity instance per transaction.
  • Lazy fetching may throw LazyInitializationException if accessed outside the persistence context.
  • Eager fetching avoids this but may load unnecessary data.

Querying with JPA

JPQL

List<Department> depts = em.createQuery("SELECT d FROM Department d JOIN FETCH d.employees", Department.class)
                           .getResultList();

Criteria API

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Department> cq = cb.createQuery(Department.class);
Root<Department> root = cq.from(Department.class);
root.fetch("employees", JoinType.LEFT);
List<Department> results = em.createQuery(cq).getResultList();

Native SQL

List<Object[]> results = em.createNativeQuery("SELECT d.name, e.name FROM departments d JOIN employees e ON d.id=e.department_id").getResultList();

Performance Considerations

  • Lazy fetching improves performance when related entities are rarely needed.
  • Eager fetching can cause cartesian product explosions with multiple joins.
  • Use JOIN FETCH in JPQL for controlled eager loading.

Real-World Use Cases

  • Lazy: Large datasets (Orders → Items) where items are accessed rarely.
  • Eager: Small, always-needed relationships (User → Profile).

Anti-Patterns and Pitfalls

  • LazyInitializationException: Accessing lazy collections outside persistence context.
  • Overusing Eager: Leads to massive joins and poor performance.
  • Unidirectional One-to-Many with Eager: Very costly in large data models.

Best Practices

  • Default to Lazy fetching.
  • Use Eager only for small, always-required associations.
  • Use JOIN FETCH in queries to optimize data loading.
  • Monitor generated SQL for tuning.

📌 JPA Version Notes

  • JPA 2.0: Introduced Criteria API, Metamodel.
  • JPA 2.1: Added Entity graphs for flexible fetching.
  • Jakarta Persistence: Package renamed from javax.persistencejakarta.persistence.

Conclusion and Key Takeaways

  • Lazy fetching = load on demand.
  • Eager fetching = load immediately.
  • Choose strategy based on data access patterns.
  • Avoid pitfalls like LazyInitializationException and cartesian joins.

By mastering fetching strategies, you ensure optimized, production-ready JPA applications.


FAQ

1. What’s the difference between JPA and Hibernate?
JPA is a specification; Hibernate is an implementation.

2. How does JPA handle the persistence context?
It caches entities and tracks changes within a transaction.

3. What are the drawbacks of eager fetching in JPA?
It loads unnecessary data, impacting memory and performance.

4. How can I solve the N+1 select problem with JPA?
Use JOIN FETCH, batch fetching, or entity graphs.

5. Can I use JPA without Hibernate?
Yes, EclipseLink and OpenJPA are alternatives.

6. What’s the best strategy for inheritance mapping in JPA?
JOINED for normalization, SINGLE_TABLE for performance.

7. How does JPA handle composite keys?
With @EmbeddedId or @IdClass.

8. What changes with Jakarta Persistence?
Namespace migrated to jakarta.persistence.

9. Is JPA suitable for microservices?
Yes, but DTOs and projections help reduce payloads.

10. When should I avoid using JPA?
In high-performance batch or NoSQL-driven apps.