N+1 Problem in Hibernate
If you’re working with Hibernate/JPA in your Java projects, you’ve definitely heard about the N+1 problem. It might be sound like a math equation, but it’s actually a performance issue that can slow down your application performance.
What is the N+1 Problem?
Let’s suppose you want to fetch a list of Customers and each Customer
has a Address
. Now ideally, Hibernate should fetch everything in one or two queries. But due to lazy loading (the default in Hibernate), here’s what happens internally:
Hibernate fires 1 query to get all Customers.
Then for each Customer, it fires another query to fetch the Address.
If there are 10 Customers, that’s 1 (initial) + 10 (for each Address) = 11 queries in total.
That’s the N+1 problem — 1 query to fetch the main(parent) data, and N queries for related(Child) data.
Why Is This Bad?
It increases the number of SQL queries.
Slows down performance — especially when data grows.
It puts unnecessary load on your database.
How to Fix It?
1. Use JOIN FETCH
@Query("SELECT c FROM Customer c LEFT JOIN FETCH c.addresses")
List fetchCustomersWithAddresses();
2. Use @EntityGraph
@EntityGraph(attributePaths = {"addresses"})
@Query("SELECT c FROM Customer c")
List findAllCustomersWithAddresses();
Below is the complete code you can refer
Customer.java
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import java.util.List;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List addresses;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List getAddresses() {
return addresses;
}
public void setAddresses(List addresses) {
this.addresses = addresses;
}
public void addAddress(Address address) {
address.setCustomer(this);
this.addresses.add(address);
}
}
Address.java
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.*;
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String city;
private String zipCode;
@ManyToOne
@JoinColumn(name = "customer_id")
@JsonBackReference
private Customer customer;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
CustomerDto.java
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CustomerDto {
private Long id;
private String name;
private List addresses;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List getAddresses() {
return addresses;
}
public void setAddresses(List addresses) {
this.addresses = addresses;
}
}
AddressDto.java
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AddressDto {
private Long id;
private String city;
private String zipCode;
private CustomerDto customer;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
public CustomerDto getCustomer() {
return customer;
}
public void setCustomer(CustomerDto customer) {
this.customer = customer;
}
}
EntityMapper.java
import org.mapstruct.BeanMapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Named;
import org.mapstruct.NullValuePropertyMappingStrategy;
import java.util.List;
public interface EntityMapper {
E toEntity(D dto);
D toDto(E entity);
List toEntity(List dtoList);
List toDto(List entityList);
@Named("partialUpdate")
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void partialUpdate(@MappingTarget E entity, D dto);
}
CustomerMapper.java
import com.satish.dto.AddressDto;
import com.satish.dto.CustomerDto;
import com.satish.entity.Address;
import com.satish.entity.Customer;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
@Mapper(componentModel = "spring")
public interface CustomerMapper extends EntityMapper{
@Mapping(source = "addresses", target = "addresses", qualifiedByName = "addressToAddressDto")
CustomerDto toDto(Customer entity);
@Named("addressToAddressDto")
@Mapping(target = "id", source = "id")
default AddressDto addressToAddressDto(Address address){
if(address == null){
return null;
}
AddressDto dto = new AddressDto();
dto.setId(address.getId());
dto.setCity(address.getCity());
dto.setZipCode(address.getZipCode());
return dto;
}
}
AddressMapper.java
import com.satish.dto.AddressDto;
import com.satish.entity.Address;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface AddressMapper extends EntityMapper{
}
CustomerController.java
import com.satish.dto.CustomerDto;
import com.satish.service.CustomerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class CustomerController {
Logger log = LoggerFactory.getLogger(CustomerController.class);
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
/**
* Saves a customer and their addresses.
* @param customerDto the customer and addresses to save
* @return the saved customer
*/
@PostMapping("/customer")
public CustomerDto saveCustomer(@RequestBody CustomerDto customerDto) {
log.info("saveCustomer() controller starts: CustomerDto: {}", customerDto);
return customerService.saveCustomer(customerDto);
}
/**
* Retrieve all customers with N+1 problem.
*
* @return a list of all customers
*/
@GetMapping("/customers/with-N-PlusOne-Problem")
public List getCustomersWithNPlusOneProblem() {
log.info("getCustomersWithNPlusOneProblem() controller starts : with N+1 Problem");
return customerService.getCustomersWithNPlusOneProblem();
}
/**
* Retrieve all customers without N+1 problem.
*
* @return a list of all customers
*/
@GetMapping("/customers/without-N-PlusOne-Problem")
public List getCustomersWithoutNPlusProblem() {
log.info("getCustomersWithoutNPlusProblem() controller starts : without N+1 Problem");
return customerService.getCustomersWithoutNPlusOneProblem();
}
@GetMapping("/fetch/customers/without-N-PlusOne-Problem")
public List fetchCustomersWithAddresses() {
log.info("fetchCustomersWithAddresses() controller starts : without N+1 Problem");
return customerService.fetchCustomersWithAddresses();
}
}
CustomerService .java
import com.satish.dto.AddressDto;
import com.satish.dto.CustomerDto;
import com.satish.entity.Customer;
import com.satish.mapper.AddressMapper;
import com.satish.mapper.CustomerMapper;
import com.satish.repository.CustomerRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomerService {
Logger log = LoggerFactory.getLogger(CustomerService.class);
private final CustomerRepository customerRepository;
private final CustomerMapper customerMapper;
private final AddressMapper addressMapper;
public CustomerService(CustomerRepository customerRepository,
CustomerMapper customerMapper,
AddressMapper addressMapper){
this.customerRepository = customerRepository;
this.customerMapper = customerMapper;
this.addressMapper = addressMapper;
}
/**
* Save a customer and it's addresses.
*
* @param customerDto a customer with it's addresses
* @return the saved customer
*/
public CustomerDto saveCustomer(CustomerDto customerDto){
log.info("saveCustomer() service starts: CustomerDto: {}", customerDto);
Customer customer = customerMapper.toEntity(customerDto);
customer.getAddresses().clear();
addAddress(customerDto, customer);
customer = customerRepository.save(customer);
return customerMapper.toDto(customer);
}
/**
* Adds a list of addresses to a customer.
*
* @param customerDto the customer dto that contains the addresses to add
* @param customer the customer to add the addresses to
*/
private void addAddress(CustomerDto customerDto, Customer customer) {
if(customerDto.getAddresses() != null && !customerDto.getAddresses().isEmpty()){
for(AddressDto addressDto : customerDto.getAddresses()){
customer.addAddress(addressMapper.toEntity(addressDto));
}
}
}
/**
* Retrieve all customers.
*
* @return a list of all customers
*/
public List getCustomersWithNPlusOneProblem() {
log.info("getCustomersWithNPlusOneProblem() service starts : with N+1 Problem");
return customerMapper.toDto(customerRepository.findAll());
}
public List getCustomersWithoutNPlusOneProblem() {
log.info("getCustomersWithoutNPlusOneProblem() service starts : without N+1 Problem");
return customerMapper.toDto(customerRepository.findAllCustomersWithAddresses());
}
public List fetchCustomersWithAddresses() {
log.info("fetchCustomersWithAddresses() service starts : without N+1 Problem");
return customerMapper.toDto(customerRepository.fetchCustomersWithAddresses());
}
}
CustomerRepository.java
import com.satish.entity.Customer;
import java.util.List;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface CustomerRepository extends JpaRepository {
// Custom method to avoid N+1 problem by using EntityGraph
@EntityGraph(attributePaths = {"addresses"})
@Query("SELECT c FROM Customer c")
List findAllCustomersWithAddresses();
@Query("SELECT c FROM Customer c LEFT JOIN FETCH c.addresses")
List fetchCustomersWithAddresses();
}
Console Output:
2025-04-19T11:57:51.009+05:30 INFO 17416 --- [nPlusOne] [nio-8080-exec-1] com.satish.service.CustomerService : getCustomersWithNPlusOneProblem() service starts : with N+1 Problem
Hibernate:
select
c1_0.id,
c1_0.name
from
customer c1_0
Hibernate:
select
a1_0.customer_id,
a1_0.id,
a1_0.city,
a1_0.zip_code
from
address a1_0
where
a1_0.customer_id=?
Hibernate:
select
a1_0.customer_id,
a1_0.id,
a1_0.city,
a1_0.zip_code
from
address a1_0
where
a1_0.customer_id=?
2025-04-19T12:00:22.416+05:30 INFO 35496 --- [nPlusOne] [nio-8080-exec-1] com.satish.service.CustomerService : ==========================================================================
2025-04-19T12:00:22.416+05:30 INFO 35496 --- [nPlusOne] [nio-8080-exec-1] com.satish.service.CustomerService : getCustomersWithoutNPlusOneProblem() service starts : without N+1 Problem
Hibernate:
select
c1_0.id,
a1_0.customer_id,
a1_0.id,
a1_0.city,
a1_0.zip_code,
c1_0.name
from
customer c1_0
left join
address a1_0
on c1_0.id=a1_0.customer_id
2025-04-19T12:01:04.388+05:30 INFO 35496 --- [nPlusOne] [nio-8080-exec-2] com.satish.service.CustomerService : ==========================================================================
2025-04-19T12:01:04.388+05:30 INFO 35496 --- [nPlusOne] [nio-8080-exec-2] com.satish.service.CustomerService : fetchCustomersWithAddresses() service starts : without N+1 Problem
Hibernate:
select
c1_0.id,
a1_0.customer_id,
a1_0.id,
a1_0.city,
a1_0.zip_code,
c1_0.name
from
customer c1_0
left join
address a1_0
on c1_0.id=a1_0.customer_id
Explanation
Initially THREE queries are executed ONE is for Parent Data(Customer) and remaining TWO queries for Child Data(Address).
But when I used @EntityGraph and JOIN FETCH query then only ONE query is get executed which fetch the data of Parent as well as of Child.