Ти знав що 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 😉️️

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