Ти знав що Spring Boot 3.5 тихо вирішив проблему N+1 запитів?

Кожен реліз спрінгбута починається з цієї новини - що спрінг самотужки вирішив проблему N+1 але це все брехня 😁
Так що в цій статті розглянемо діючі варіанти вирішення N+1
Почнемо з прикладу
Уявімо два класи: Author та Book, де один автор може мати багато книг.
@Entity
public class Author {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books = new ArrayList<>();
}
@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;
    private String title;
    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
}
І тепер — типова ситуація:
List<Author> authors = authorRepository.findAll();
for (Author author : authors) {
    System.out.println(author.getBooks().size());
}Здається, усе просто: ми хочемо порахувати, скільки книжок має кожен автор. Але під капотом Hibernate зробить:
1 запит для отримання всіх авторів,
і ще N окремих запитів — по одному для кожного автора, щоби дістати його книги.
❌ І тут ми маємо ту саму N+1 проблему — 1 + N запитів до бази.
Варіанти вирішення
1. JOIN FETCH — швидко і надійно
Це найпоширеніший спосіб — просто вказати у JPQL, що ми хочемо підвантажити асоційовану колекцію одразу.
public interface AuthorRepository extends JpaRepository<Author, Long> {
    // 1. Стандартний запит (викликає N+1 при LAZY)
    List<Author> findAll();
    // 2. JOIN FETCH - один запит
    @Query("SELECT a FROM Author a JOIN FETCH a.books")
    List<Author> findAllWithBooks();
}
Hibernate згенерує SQL приблизно такого вигляду:
SELECT a.*, b.*
FROM author a
JOIN book b ON b.author_id = a.id
2. EntityGraph — декларативний і елегантний
EntityGraph дозволяє вказати потрібні звʼязки анотаціями, а не в JPQL. Це читається набагато краще та гнучкіше.
Статичний варіант:
@NamedEntityGraph(
    name = "Author.withBooks",
    attributeNodes = @NamedAttributeNode("books")
)
@Entity
public class Author {
    ...
}
public interface AuthorRepository extends JpaRepository<Author, Long> {
    // EntityGraph (оголошений у @NamedEntityGraph в Author)
    @EntityGraph(value = "Author.withBooks", type = EntityGraph.EntityGraphType.LOAD)
    List<Author> findAllUsingEntityGraph();
}Або ще простіше — динамічний (без @NamedEntityGraph):
public interface AuthorRepository extends JpaRepository<Author, Long> {
    // Динамічний EntityGraph (без @NamedEntityGraph)
    @EntityGraph(attributePaths = "books")
    List<Author> findByNameContaining(String name);
}
SQL запит буде такий самий, як у JOIN FETCH, але підхід декларативний і зручніший при потребі розширення.
3. Batch Fetching — повільно, але безпечно
нколи хочеться не переписувати запити, а просто дати Hibernate змогу самостійно групувати lazy-запити.
Ось як це зробити:
У application.properties:
spring.jpa.properties.hibernate.default_batch_fetch_size=16Результат:
Замість 1 + N запитів, Hibernate зробить:
1 запит для авторів
1 або кілька запитів на
booksпо групах
SQL, який буде згенеровано:
select * from author;
select * from book where author_id in (?, ?, ..., ?);Тут зверни увагу чим більший batch size тим більший in
4. DTO-проєкції — коли потрібні лише окремі поля
Часто вам не потрібні всі поля авторів і книжок. Можливо, ви просто хочете показати Author.name і Book.title. Тоді — проєкції вам у поміч.
public record AuthorBooksDto(String authorName, String bookTitle) {}
@Query("""
    SELECT new com.example.dto.AuthorBooksDto(a.name, b.title)
    FROM Author a
    JOIN a.books b
""")
List<AuthorBooksDto> fetchAuthorBooks();Hibernate не завантажує ентіті взагалі — тільки те, що потрібно для побудови DTO.
📊 Порівняння стратегій
Підхід  | Простота  | Продуктивність  | Гнучкість  | Коли використовувати  | 
|---|---|---|---|---|
  | ✅ Прямолінійний  | ✅✅✅ Один запит  | ⚠️ Лише одна колекція  | Коли потрібна вся інформація одразу  | 
  | ✅✅ Читабельний  | ✅✅✅ Динамічний  | ✅✅ Можна комбінувати  | Для складних або розширюваних запитів  | 
  | ✅✅✅ Мінімально-інвазивний  | ✅✅ Груповані запити  | ⚠️ Менше контролю  | Якщо хочеться залишити все як є  | 
  | ⚠️ Більше роботи  | ✅✅✅ Максимальна швидкість  | ✅✅ Повна свобода  | Для API, списків, аналітики  | 
Висновки
Краще використовуй MyBatis або JOOQ 😉️️

Хочеш мене підтримати підписуйся на мій патреон:

