What Fetch Strategies Exist in Hibernate
Fetch strategies determine how Hibernate loads related entities from the database.
Overview
Fetch strategies determine how Hibernate loads related entities from the database.
FetchType (JPA) is about “when” - immediately or on access. FetchMode (Hibernate) is about “how” - in one query, separately, or in batches. Understanding fetch strategies is key to optimizing performance and preventing issues like N+1.
Junior Level
Basic Strategies
Hibernate provides 4 fetch strategies:
- SELECT - separate query on association access (default for LAZY)
- JOIN - single query with JOIN (default for EAGER)
- SUBSELECT - subquery for a collection
- BATCH - batch loading of multiple entities
Important: BATCH is not a FetchMode, but a separate mechanism via @BatchSize.
Real classification:
- FetchMode (Hibernate-specific): SELECT, JOIN, SUBSELECT
- Separate mechanism: @BatchSize (batch loading)
- Dynamic: EntityGraph (JPA standard)
// SELECT - separate query on access
@OneToMany(fetch = FetchType.LAZY)
@Fetch(FetchMode.SELECT)
private List<OrderItem> items;
// JOIN - single query with JOIN
@OneToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.JOIN)
private List<OrderItem> items;
// SUBSELECT - subquery for collection
@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items;
Difference Between FetchType and FetchMode
| FetchType (JPA) | @Fetch (Hibernate) | |
|---|---|---|
| Standard | JPA standard | Hibernate-specific |
| Values | LAZY, EAGER | SELECT, JOIN, SUBSELECT |
| When | When to load | How to load |
Middle Level
Detailed FetchMode Description
SELECT
@Fetch(FetchMode.SELECT)
// Separate SELECT for each entity
// Loading 10 Orders -> 10 SELECTs for items
// Causes N+1 problem!
JOIN
@Fetch(FetchMode.JOIN)
// Single query with JOIN
// SELECT o.*, i.* FROM orders o JOIN order_items i ON o.id = i.order_id
// Important: JOIN ignores FetchType.LAZY!
// Even if LAZY is specified, it still loads immediately
// In Hibernate 5.x JOIN ignores LAZY. In Hibernate 6+ behavior is refined, but for guaranteed lazy use JOIN FETCH in JPQL.
SUBSELECT
@Fetch(FetchMode.SUBSELECT)
// One subquery for the entire collection
// SELECT * FROM order_items WHERE order_id IN (SELECT id FROM orders)
// Useful when multiple parent entities are loaded
// SUBSELECT only works if parent entities were loaded in a single query.
// If loaded one by one - Hibernate falls back to SELECT.
Practical Examples
@Entity
public class Order {
// LAZY + SELECT = N+1 (bad)
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@Fetch(FetchMode.SELECT)
private List<OrderItem> items;
// LAZY + JOIN = always JOIN (ignores LAZY)
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@Fetch(FetchMode.JOIN)
private List<OrderItem> items;
// LAZY + SUBSELECT = one subquery
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items;
}
Common Mistakes
// FetchMode.JOIN ignores LAZY
@Fetch(FetchMode.JOIN)
@OneToMany(fetch = FetchType.LAZY)
// Still loads immediately via JOIN
// Using without understanding consequences
@Fetch(FetchMode.SUBSELECT)
@ManyToOne
// SUBSELECT only makes sense for collections
Senior Level
Internal Implementation
When there’s a conflict, FetchMode overrides FetchType. LAZY + JOIN = still JOIN. This is a common mistake.
FetchMode.JOIN:
- On entity load, immediately does LEFT/INNER JOIN
- Completely ignores FetchType.LAZY
- Result: all related entities are loaded
- Suitable when association is needed 100% of the time
FetchMode.SELECT:
- On first proxy access
- Executes SELECT WHERE foreign_key = ?
- Separate query for each entity
- Causes N+1 problem
FetchMode.SUBSELECT:
- Collects all IDs of loaded entities
- One query: WHERE parent_id IN (id1, id2, ...)
- Efficient for small parent collections
Global Configuration
# application.yml
spring:
jpa:
properties:
hibernate:
# Global batch size for all LAZY associations
default_batch_fetch_size: 50
# Or via Hibernate annotations
# @BatchSize(size = 50) at class/collection level
Advanced Strategies
Batch fetching (recommended approach)
@OneToMany(mappedBy = "order")
@BatchSize(size = 25)
private List<OrderItem> items;
// Instead of 100 queries -> 4 queries (100/25)
// Hibernate: SELECT * FROM order_items WHERE order_id IN (?, ..., ?) -- 25 IDs
Dynamic fetching via EntityGraph
// Static EntityGraph
@NamedEntityGraph(
name = "Order.withItems",
attributeNodes = @NamedAttributeNode("items")
)
@Entity
public class Order { }
// Usage
EntityGraph<Order> graph = entityManager.getEntityGraph("Order.withItems");
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class)
.setHint("jakarta.persistence.fetchgraph", graph)
.getResultList();
Comparison of Approaches
| Strategy | Queries | Flexibility | When to use |
|---|---|---|---|
| SELECT | N+1 | No | When association is not needed (LAZY default) |
| JOIN | 1 | No | Association always needed |
| SUBSELECT | 1 | No | Few parents |
| BATCH | N/batchSize | Yes | Collections |
| EntityGraph | 1 | Yes | Dynamic loading |
| JOIN FETCH | 1 | Yes | JPQL queries |
Best Practices
@BatchSize for collections (10-50)
LAZY + JOIN FETCH in JPQL queries
EntityGraph for dynamic loading
SUBSELECT for rare cases
FetchMode.JOIN without reason (ignores LAZY)
FetchMode.SELECT (causes N+1)
Mixing EAGER + JOIN
Global EAGER without necessity
Interview Cheat Sheet
Must know:
- FetchType (JPA): LAZY, EAGER - determines “when” to load
- FetchMode (Hibernate): SELECT, JOIN, SUBSELECT - determines “how” to load
- FetchMode.JOIN ignores LAZY - common mistake
- @BatchSize - separate mechanism, not FetchMode, loads in batches
- EntityGraph - dynamic loading, most flexible approach
- On conflict, FetchMode overrides FetchType
Frequent follow-up questions:
- Difference FetchType vs FetchMode? FetchType - JPA standard (when), FetchMode - Hibernate-specific (how)
- Why does FetchMode.JOIN ignore LAZY? JOIN immediately does a JOIN in SQL, LAZY cannot work
- What is SUBSELECT? One subquery for all loaded parents: WHERE parent_id IN (SELECT id FROM parents)
- When to use EntityGraph? When you need to dynamically choose what to load in different scenarios
Red flags (DO NOT say):
- “FetchMode.JOIN + LAZY = lazy loading” - JOIN ignores LAZY
- “I use FetchMode.SELECT by default” - causes N+1
- “@BatchSize is a FetchMode” - it’s a separate mechanism
- “EAGER + JOIN is the best combination” - antipattern
Related topics:
- [[1. What is the N+1 Problem and How to Solve It]]
- [[2. What is the Difference Between Lazy and Eager Loading]]
- [[6. What Does the @BatchSize Annotation Do]]
- [[28. How to Use JOIN FETCH to Solve the N+1 Problem]]