Learnitweb

Hibernate N+1 Problem

Introduction

The N+1 problem in Hibernate is a common performance issue that occurs when an application executes excessive database queries due to improper handling of relationships. This can severely degrade application performance, especially when dealing with large datasets.

Understanding the N+1 Problem

What is the N+1 Problem?

The N+1 problem occurs when Hibernate retrieves a list of parent entities, but then issues an additional query for each related child entity. This results in N+1 queries, where N is the number of parent entities.

Example Scenario

Consider two entities: Author and Book. Each Author has multiple Book entries.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Entity
@Table(name = "authors")
data class Author(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int,
val name: String,
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
val books: List<Book> = emptyList()
)
@Entity
@Table(name = "books")
data class Book(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int,
val title: String,
@ManyToOne
@JoinColumn(name = "author_id")
val author: Author
)
@Entity @Table(name = "authors") data class Author( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Int, val name: String, @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) val books: List<Book> = emptyList() ) @Entity @Table(name = "books") data class Book( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Int, val title: String, @ManyToOne @JoinColumn(name = "author_id") val author: Author )
@Entity
@Table(name = "authors")
data class Author(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int,

    val name: String,

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    val books: List<Book> = emptyList()
)

@Entity
@Table(name = "books")
data class Book(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int,

    val title: String,

    @ManyToOne
    @JoinColumn(name = "author_id")
    val author: Author
)

How the N+1 Problem Occurs

If we retrieve all authors using Hibernate:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val authors = entityManager.createQuery("SELECT a FROM Author a", Author::class.java).resultList
for (author in authors) {
println("Author: ${author.name}, Books: ${author.books.size}")
}
val authors = entityManager.createQuery("SELECT a FROM Author a", Author::class.java).resultList for (author in authors) { println("Author: ${author.name}, Books: ${author.books.size}") }
val authors = entityManager.createQuery("SELECT a FROM Author a", Author::class.java).resultList

for (author in authors) {
    println("Author: ${author.name}, Books: ${author.books.size}")
}

Here’s what happens:

  • Hibernate executes one query to fetch all authors:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT * FROM authors;
SELECT * FROM authors;
SELECT * FROM authors;
  • For each author, it runs an additional query to fetch their books:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT * FROM books WHERE author_id = ?;
SELECT * FROM books WHERE author_id = ?;
SELECT * FROM books WHERE author_id = ?;

This happens N times (once for each author), leading to N+1 queries.

Solutions to the N+1 Problem

Using JOIN FETCH (Recommended Approach)

We can use JOIN FETCH to fetch both authors and books in a single query:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val authors = entityManager.createQuery("SELECT a FROM Author a JOIN FETCH a.books", Author::class.java).resultList
val authors = entityManager.createQuery("SELECT a FROM Author a JOIN FETCH a.books", Author::class.java).resultList
val authors = entityManager.createQuery("SELECT a FROM Author a JOIN FETCH a.books", Author::class.java).resultList

Generated SQL:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SELECT a.*, b.* FROM authors a JOIN books b ON a.id = b.author_id;
SELECT a.*, b.* FROM authors a JOIN books b ON a.id = b.author_id;
SELECT a.*, b.* FROM authors a JOIN books b ON a.id = b.author_id;

This eliminates the N+1 problem by loading the related entities in a single query.

Using EntityGraph (Dynamic Fetching)

EntityGraph allows selective fetching of relationships at runtime:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val entityGraph = entityManager.createEntityGraph(Author::class.java)
entityGraph.addSubgraph("books")
val query = entityManager.createQuery("SELECT a FROM Author a", Author::class.java)
query.setHint("javax.persistence.fetchgraph", entityGraph)
val authors = query.resultList
val entityGraph = entityManager.createEntityGraph(Author::class.java) entityGraph.addSubgraph("books") val query = entityManager.createQuery("SELECT a FROM Author a", Author::class.java) query.setHint("javax.persistence.fetchgraph", entityGraph) val authors = query.resultList
val entityGraph = entityManager.createEntityGraph(Author::class.java)
entityGraph.addSubgraph("books")

val query = entityManager.createQuery("SELECT a FROM Author a", Author::class.java)
query.setHint("javax.persistence.fetchgraph", entityGraph)

val authors = query.resultList

Batch Size Optimization with @BatchSize

Another approach is to use @BatchSize, which loads related entities in batches:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@BatchSize(size = 10)
val books: List<Book> = emptyList()
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @BatchSize(size = 10) val books: List<Book> = emptyList()
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@BatchSize(size = 10)
val books: List<Book> = emptyList()

This reduces the number of queries by batching related entities together.

Using @Fetch(FetchMode.SUBSELECT)

For OneToMany relationships, FetchMode.SUBSELECT loads related entities in a single query per batch:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
val books: List<Book> = emptyList()
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @Fetch(FetchMode.SUBSELECT) val books: List<Book> = emptyList()
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
val books: List<Book> = emptyList()

This generates one query per batch instead of N queries.