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:

  1. Hibernate fires 1 query to get all Customers.

  2. 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<Customer> fetchCustomersWithAddresses();
				
			

2. Use @EntityGraph

				
					@EntityGraph(attributePaths = {"addresses"})
    @Query("SELECT c FROM Customer c")
    List<Customer> 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<Address> 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<Address> getAddresses() {
        return addresses;
    }

    public void setAddresses(List<Address> 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<AddressDto> 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<AddressDto> getAddresses() {
        return addresses;
    }

    public void setAddresses(List<AddressDto> 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<D, E> {

    E toEntity(D dto);

    D toDto(E entity);

    List<E> toEntity(List<D> dtoList);

    List<D> toDto(List<E> 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<CustomerDto, Customer>{

    @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<AddressDto, Address>{
}
				
			

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<CustomerDto> 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<CustomerDto> getCustomersWithoutNPlusProblem() {
        log.info("getCustomersWithoutNPlusProblem() controller starts : without N+1 Problem");
        return customerService.getCustomersWithoutNPlusOneProblem();
    }

    @GetMapping("/fetch/customers/without-N-PlusOne-Problem")
    public List<CustomerDto> 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<CustomerDto> getCustomersWithNPlusOneProblem() {
        log.info("getCustomersWithNPlusOneProblem() service starts : with N+1 Problem");
        return customerMapper.toDto(customerRepository.findAll());
    }


    public List<CustomerDto> getCustomersWithoutNPlusOneProblem() {
        log.info("getCustomersWithoutNPlusOneProblem() service starts : without N+1 Problem");
        return customerMapper.toDto(customerRepository.findAllCustomersWithAddresses());
    }

    public List<CustomerDto> 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<Customer, Long> {

    // Custom method to avoid N+1 problem by using EntityGraph
    @EntityGraph(attributePaths = {"addresses"})
    @Query("SELECT c FROM Customer c")
    List<Customer> findAllCustomersWithAddresses();

    @Query("SELECT c FROM Customer c LEFT JOIN FETCH c.addresses")
    List<Customer> 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.
				
			
Scroll to Top