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.

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

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:
SELECT * FROM authors;
  • For each author, it runs an additional query to fetch their books:
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:

val authors = entityManager.createQuery("SELECT a FROM Author a JOIN FETCH a.books", Author::class.java).resultList

Generated SQL:

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:

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:

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

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

This generates one query per batch instead of N queries.