Question 5 · Section 16

What Fetch Strategies Exist in Hibernate

Fetch strategies determine how Hibernate loads related entities from the database.

Language versions: English Russian Ukrainian

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:

  1. SELECT - separate query on association access (default for LAZY)
  2. JOIN - single query with JOIN (default for EAGER)
  3. SUBSELECT - subquery for a collection
  4. 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

@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]]