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
| State | Description |
|---|---|
| Transient | New object |
| Managed | Attached to session |
| Detached | Session closed |
| Removed | Scheduled 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
| Relationship | Default |
|---|---|
| ManyToOne | EAGER |
| OneToOne | EAGER |
| OneToMany | LAZY |
| ManyToMany | LAZY |
Recommended Fetch Strategy
| Relationship | Recommendation |
|---|---|
| ManyToOne | LAZY |
| OneToOne | LAZY |
| OneToMany | LAZY |
| ManyToMany | LAZY |
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:
| Cache | Scope |
|---|---|
| First Level | Session |
| Second Level | Application |
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
| Check | Recommended |
|---|---|
| LAZY relationships | Yes |
| DTO projections | Yes |
| Pagination | Yes |
| Batch fetching | Yes |
| Entity graph | Yes |
| Join fetch | Yes |
| OSIV disabled | Yes |
| SQL logging | Yes |
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