Part 8 – Lazy Loading, N+1 Queries and Hibernate Performance Tuning


Introduction

Most JPA applications work perfectly during development.

Problems begin in production:

  • Pages become slow.
  • APIs execute hundreds of SQL statements.
  • CPU usage increases.
  • Database load increases.
  • Memory usage grows.
  • Connection pools become exhausted.

In many cases, the root cause is not Oracle.

The root cause is:

❌ Improper fetch strategy

❌ N+1 queries

❌ EAGER loading

❌ Large object graphs

❌ Missing pagination

❌ Incorrect entity design

This article covers:

✅ Lazy loading

✅ Eager loading

✅ N+1 problem

✅ Join fetch

✅ Entity graphs

✅ DTO projections

✅ Batch fetching

✅ Second-level cache

✅ Hibernate statistics


Persistence Context

Hibernate maintains a persistence context.

Database
    ↓
Hibernate Session
    ↓
Entity Objects

Entities inside the session are:

  • Managed
  • Cached
  • Automatically synchronized

Entity States

StateDescription
TransientNew object
ManagedAttached to session
DetachedSession closed
RemovedScheduled for delete

Fetch Types

JPA supports:

FetchType.LAZY

FetchType.EAGER

EAGER Loading

@ManyToOne(
    fetch = FetchType.EAGER)
private Department department;

Loading employee:

Employee
Department
Manager
Location

Everything loads immediately.


Problems with EAGER

Suppose:

100 Employees

Each employee:

  • Department
  • Manager
  • Location

Potential queries:

301 SQL statements

LAZY Loading

@ManyToOne(
    fetch = FetchType.LAZY)
private Department department;

Department loads only when accessed.


Example

Employee employee =
    repository.findById(1L);

No department query yet.

Later:

employee.getDepartment();

Hibernate executes:

SELECT *
FROM DEPARTMENT
WHERE DEPT_ID = ?

Default Fetch Types

RelationshipDefault
ManyToOneEAGER
OneToOneEAGER
OneToManyLAZY
ManyToManyLAZY

Recommended Fetch Strategy

RelationshipRecommendation
ManyToOneLAZY
OneToOneLAZY
OneToManyLAZY
ManyToManyLAZY

The N+1 Query Problem

Suppose:

List<Employee> employees =
    repository.findAll();

for(Employee e : employees) {

    e.getDepartment()
     .getName();
}

Generated SQL

SELECT *
FROM EMPLOYEE;

Then:

SELECT *
FROM DEPARTMENT
WHERE DEPT_ID = 10;
SELECT *
FROM DEPARTMENT
WHERE DEPT_ID = 20;

100 employees:

1 + 100 queries

Why N+1 is Dangerous

  • Database load
  • Network overhead
  • Slow APIs
  • High latency

Join Fetch Solution

@Query("""
select e
from Employee e
join fetch e.department
""")
List<Employee> findAllWithDepartment();

Generated SQL:

SELECT *
FROM EMPLOYEE E
JOIN DEPARTMENT D
ON E.DEPT_ID=D.DEPT_ID

Single query.


Entity Graph

@EntityGraph(
    attributePaths = "department")
List<Employee> findAll();

Benefits:

  • Cleaner code.
  • Dynamic fetching.
  • Better performance.

DTO Projection

Instead of:

Employee

Return:

EmployeeDto

Example

public record EmployeeDto(
    Long id,
    String name,
    String department) {
}

Query:

@Query("""
select new
com.demo.EmployeeDto(
e.id,
e.name,
d.name)
from Employee e
join e.department d
""")

Advantages

✅ Smaller objects

✅ Less memory

✅ Faster serialization

✅ Better APIs


LazyInitializationException

Example:

Employee employee =
    repository.findById(1L);

session.close();

employee.getDepartment();

Result:

LazyInitializationException

Why?

Hibernate session is closed.

Proxy cannot load data.


Solutions

Join Fetch

Recommended.


Entity Graph

Recommended.


DTO Projection

Recommended.


Open Session in View

Avoid.


Open Session in View

Spring:

spring.jpa.open-in-view=true

Problems:

  • Long transactions.
  • Unexpected SQL.
  • Performance issues.

Recommended:

spring.jpa.open-in-view=false

Batch Fetching

Example:

hibernate.default_batch_fetch_size=50

Instead of:

100 queries

Hibernate:

2 queries

Collection Batch Example

@OneToMany(
    fetch = FetchType.LAZY)
private List<OrderItem> items;

Batch fetching reduces SQL.


Pagination

Bad:

repository.findAll();

Millions of rows.


Good:

Page<Employee> page =
    repository.findAll(
        PageRequest.of(0,20));

SQL:

FETCH FIRST 20 ROWS ONLY

Streaming Results

Large reports:

Stream<Employee> stream;

Benefits:

  • Reduced memory.
  • Better scalability.

Second-Level Cache

Levels:

CacheScope
First LevelSession
Second LevelApplication

First-Level Cache

Default.

employee1 =
entityManager.find();

employee2 =
entityManager.find();

One SQL.


Second-Level Cache

Libraries:

  • EhCache
  • Redis
  • Hazelcast

Enable Cache

hibernate.cache.use_second_level_cache=true

Query Cache

hibernate.cache.use_query_cache=true

Use carefully.


Hibernate Statistics

Enable:

hibernate.generate_statistics=true

Logs:

Queries executed

Cache hits

Entity loads

SQL Logging

logging.level.org.hibernate.SQL=DEBUG

Parameters:

logging.level.org.hibernate.orm.jdbc.bind=TRACE

Connection Pool Impact

Poor fetch strategies:

More SQL
    ↓
More connections
    ↓
Pool exhaustion

Recommended Architecture

Controller
      ↓
Service
      ↓
Repository
      ↓
DTO Query
      ↓
Database

Avoid:

Controller
      ↓
Entity
      ↓
Lazy loading

Common Mistakes

EAGER Everywhere

Bad.


Returning Entities to REST

Bad.


Missing Pagination

Bad.


Open Session in View

Bad.


Large Object Graphs

Bad.


Performance Checklist

CheckRecommended
LAZY relationshipsYes
DTO projectionsYes
PaginationYes
Batch fetchingYes
Entity graphYes
Join fetchYes
OSIV disabledYes
SQL loggingYes

Interview Questions

What is the N+1 problem?

One query plus N additional queries.


Why use LAZY loading?

Load only required data.


Why avoid EAGER?

Excessive SQL.


What causes LazyInitializationException?

Session closed.


What is join fetch?

Loads associations in one query.


Why use DTO projections?

Smaller result objects.


Difference between first-level and second-level cache?

Session cache versus application cache.


Best Practices

✅ Make all relationships LAZY.

✅ Use DTO projections.

✅ Use pagination.

✅ Monitor generated SQL.

✅ Use join fetch.

✅ Enable batch fetching.

✅ Disable OSIV.

✅ Use entity graphs.


Summary

This article covered:

✅ Entity states

✅ Lazy loading

✅ Eager loading

✅ N+1 queries

✅ Join fetch

✅ Entity graphs

✅ DTO projections

✅ Pagination

✅ Batch fetching

✅ Hibernate cache


Next Article

Part 9 – Transactions, Locking and Concurrency Control

Topics:

  • ACID properties
  • Transaction boundaries
  • Spring @Transactional
  • Isolation levels
  • Propagation
  • Optimistic locking
  • Pessimistic locking
  • Deadlocks
  • Lost updates
  • Concurrency management

Leave a Reply

Your email address will not be published. Required fields are marked *