Sobes.tech
Middle+

Масъли N+1 дар Hibernate чист ва чӣ гуна метавон онро ҳал кард?

sobes.tech AI

Ҷавоб аз AI

Проблема N+1 у Hibernate виникає при вибірці даних, коли для отримання батьківських об'єктів виконується один запит, а потім для кожного дитини (або колекції дітей) виконується окремий запит. Це призводить до N+1 запитів до бази даних, де N — кількість батьківських об'єктів, замість оптимального одного або кількох запитів з об'єднаннями.

Приклад сценарію з проблемою N+1:

Уявімо, що є класи Author і Book, де у автора може бути багато книг.

// Author entity
@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // LAZY fetch за замовчуванням для колекцій
    private List<Book> books;

    // getters і setters
}

// Book entity
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToOne
    private Author author;

    // getters і setters
}

Якщо ми хочемо вивести всіх авторів і назви їх книг:

Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 запит до Author

for (Author author : authors) {
    System.out.println("Author: " + author.getName());
    for (Book book : author.getBooks()) { // N запитів до Book (по одному на кожного автора)
        System.out.println("- Book: " + book.getTitle());
    }
}
session.close();

У цьому прикладі:

  1. Виконується один запит для отримання всіх Author.
  2. Потім у циклі для кожного Author виконується окремий запит для завантаження колекції books. Якщо у нас 100 авторів, буде виконано 100 додаткових запитів до таблиці Book. Всього 1 (для авторів) + 100 (для книг) = 101 запит.

Рішення проблеми N+1:

  1. Використання JOIN FETCH у JPQL/HQL: Явно завантажує пов'язані сутності за один запит.

    Session session = sessionFactory.openSession();
    List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // Завантажує Author і їх Books за один запит
    
    for (Author author : authors) {
        System.out.println("Author: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Book: " + book.getTitle());
        }
    }
    session.close();
    

    Оператор DISTINCT використовується для запобігання дублюванню рядків у результаті запиту, що може виникнути при об'єднанні один-до-многих.

  2. Зміна типу завантаження на EAGER: Зміна fetch = FetchType.LAZY (за замовчуванням для колекцій) на fetch = FetchType.EAGER.

    @Entity
    public class Author {
        // ... інші поля
        @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // EAGER fetch
        private List<Book> books;
        // ... getters і setters
    }
    

    Не рекомендується для колекцій або сутностей із великим числом зв'язків, оскільки може призвести до завантаження зайвих даних і проблем з продуктивністю (ефект "каратезіанського добутку"). Підходить для зв'язків ManyToOne/OneToOne, де очікується, що пов'язаний об'єкт завжди потрібен.

  3. Використання FetchMode у Criteria API: Дозволяє вказати, як мають завантажуватися пов'язані сутності.

    Session session = sessionFactory.openSession();
    Criteria criteria = session.createCriteria(Author.class)
        .setFetchMode("books", FetchMode.JOIN); // Використовує LEFT OUTER JOIN для завантаження books
    
    List<Author> authors = criteria.list();
    
    for (Author author : authors) {
        System.out.println("Author: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Book: " + book.getTitle());
        }
    }
    session.close();
    
  4. Використання BatchSize аннотації: Вказує Hibernate завантажувати пов'язані об'єкти (або колекції) групами певного розміру, що зменшує кількість запитів, хоча й не зводить їх до одного.

    @Entity
    @BatchSize(size = 10) // Hibernate буде завантажувати Author пакетами по 10
    public class Author {
        // ... інші поля
        @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
        @BatchSize(size = 10) // Hibernate буде завантажувати books для пакетів Author по 10
        private List<Book> books;
        // ... getters і setters
    }
    

    При обході колекції books для першого автора, Hibernate завантажить books одразу для наступних 9 авторів (якщо вони були завантажені у тій же сесії). Це значно зменшує кількість запитів у порівнянні з N+1.

  5. Використання Entity Graphs: Дозволяє явно визначити, які пов'язані об'єкти або колекції мають бути завантажені при виконанні запиту.

    @NamedEntityGraph(name = "author-with-books",
        attributeNodes = @NamedAttributeNode("books")
    )
    @Entity
    public class Author {
        // ... поля та зв'язки
    }
    
    Session session = sessionFactory.openSession();
    jakarta.persistence.EntityGraph<Author> entityGraph = session.createEntityGraph(Author.class);
    entityGraph.addAttributeNodes("books");
    
    List<Author> authors = session.createQuery("SELECT a FROM Author a", Author.class)
        .setHint("jakarta.persistence.fetchgraph", entityGraph) // або fetchgraph залежно від бажаної поведінки
        .getResultList();
    
    for (Author author : authors) {
        System.out.println("Author: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Book: " + book.getTitle());
        }
    }
    session.close();
    

Вибір конкретного рішення залежить від контексту, співвідношення один-до-одного/один-до-багатьох/багато-до-багатьох, обсягу даних і потрібної гнучкості. JOIN FETCH і Entity Graphs часто є переважними для завантаження всіх пов'язаних даних в одному запиті, тоді як BatchSize ефективний при роботі з великою кількістю сутностей і може бути корисним, коли JOIN FETCH призводить до надмірних результатів. EAGER завантаження слід використовувати обережно.