batch fetching is a great way to avoid the N+1 problem without forcing eager loading. Let’s break it down clearly:
🔎 What is batch fetching?
When you use lazy loading, Hibernate loads collections or associated entities on demand — but by default, it issues 1 query per entity → classic N+1 problem.
Batch fetching lets Hibernate load multiple entities or collections in batches when one is accessed, reducing the number of queries.
For example:
- Without batch fetching: loading 100 lazy
Author.books
→ 100 separate queries. - With batch fetching size 10: loading those 100 → only ~10 queries (each loads 10 collections at once).
✅ How to enable batch fetching
You do it in your entity mappings using Hibernate’s @BatchSize
annotation or globally in configuration:
1️⃣ Per-association: @BatchSize
For an entity or collection field:
@Entity
public class Author {
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@BatchSize(size = 10) // Hibernate will load books for 10 authors at once
private List<Book> books;
}
Or on an entity to affect all its lazy references:
@Entity
@BatchSize(size = 10)
public class Book {
// ...
}
2️⃣ Global setting: Hibernate property
If you want batch fetching to apply to all lazy associations in your app, set in your config (e.g., application.properties
):
spring.jpa.properties.hibernate.default_batch_fetch_size=10
Or directly in Hibernate:
hibernate.default_batch_fetch_size=10
🚨 Important notes:
✔ Batch fetching ≠ eager fetching — it keeps associations lazy, but batches the loads when needed.
✔ Works only if you access multiple lazy proxies of the same type in the same session.
✔ Doesn’t change JPQL behavior — it applies when Hibernate resolves proxies or collections.
✔ Very helpful for reducing query count in web pages/tables showing multiple entities with lazy fields.
🔎 Example:
You load 20 authors, then access author.getBooks()
in a loop — instead of 20 queries, Hibernate will do 2 queries (if batch size is 10).
🛠 Example scenario
You have:
- Entity
Author
- Each author has many lazy-loaded
Book
entities (@OneToMany(fetch = LAZY)
).
❌ Without batch fetching
List<Author> authors = entityManager.createQuery(
"SELECT a FROM Author a", Author.class
).getResultList();
for (Author author : authors) {
List<Book> books = author.getBooks(); // triggers lazy load for each author
System.out.println("Author: " + author.getName() + ", books count: " + books.size());
}
If authors.size() == 20
, Hibernate will run:
- 1 query for authors,
- 20 separate queries for each author’s books,
- total: 21 queries.
✅ With batch fetching
First, enable batching with @BatchSize
on the collection:
@Entity
public class Author {
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@BatchSize(size = 5) // fetch 5 authors’ books in one query
private List<Book> books;
}
Then, run the same code:
List<Author> authors = entityManager.createQuery(
"SELECT a FROM Author a", Author.class
).getResultList();
for (Author author : authors) {
List<Book> books = author.getBooks();
System.out.println("Author: " + author.getName() + ", books count: " + books.size());
}
Now, Hibernate groups lazy loads into batches of 5:
- 1 query for authors,
- 4 queries for books (because 20 authors / batch size 5 = 4 batches),
- total: 5 queries, not 21.
🔎 SQL logs comparison
Without batch:
-- Query 1: fetch authors
SELECT * FROM authors;
-- Then for each author (20 times):
SELECT * FROM books WHERE author_id = ?;
...
With batch size 5:
-- Query 1: fetch authors
SELECT * FROM authors;
-- Batch 1: books for authors 1-5
SELECT * FROM books WHERE author_id IN (?, ?, ?, ?, ?);
-- Batch 2: authors 6-10
SELECT * FROM books WHERE author_id IN (?, ?, ?, ?, ?);
...
(total 4 batches)
✅ Why it matters
- Greatly reduces database round trips.
- Keeps your associations lazy (good for flexibility) but avoids N+1 problem.