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

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

Создать мем "котики, коты, я тебе напиздел" - Картинки - Meme-arsenal.com
Хехехе

Кожен реліз спрінгбута починається з цієї новини - що спрінг самотужки вирішив проблему 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.

📊 Порівняння стратегій

Підхід

Простота

Продуктивність

Гнучкість

Коли використовувати

JOIN FETCH

✅ Прямолінійний

✅✅✅ Один запит

⚠️ Лише одна колекція

Коли потрібна вся інформація одразу

EntityGraph

✅✅ Читабельний

✅✅✅ Динамічний

✅✅ Можна комбінувати

Для складних або розширюваних запитів

Batch Fetching

✅✅✅ Мінімально-інвазивний

✅✅ Груповані запити

⚠️ Менше контролю

Якщо хочеться залишити все як є

DTO-проєкції

⚠️ Більше роботи

✅✅✅ Максимальна швидкість

✅✅ Повна свобода

Для API, списків, аналітики

Висновки

Краще використовуй MyBatis або JOOQ 😉️️

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

Хехе пастка на лінкедін
Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Сергій Барабаш
Сергій Барабаш@sbarabash

582Прочитань
1Автори
12Читачі
На Друкарні з 30 березня

Більше від автора

Вам також сподобається

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається