Sobes.tech
Middle+

Ի՞նչ է Hibernate-ում N+1 խնդիրը և ինչպես կարելի է այն լուծել։

sobes.tech AI

Պատասխան AI-ից

Hibernate- ում N+1 խնդիրը առաջանում է տվյալների ընտրության ժամանակ, երբ ծնողական օբյեկտների ստացման համար կատարվում է մեկ հարցում, իսկ յուրաքանչյուր երեխայի (կամ երեխաների հավաքածուի) համար՝ առանձին հարցում: Սա հանգեցնում է 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 բեռնում
        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՝ բեռնելու համար
    
    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 անոտացիա:

    @Entity
    @BatchSize(size = 10) // Hibernate-ը բեռնում է Author-ներին խմբերով՝ 10-ական
    public class Author {
        // ... այլ դաշտեր
        @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
        @BatchSize(size = 10) // Hibernate-ը բեռնում է `books`՝ խմբերով՝ 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 բեռնումը պետք է օգտագործել զգուշությամբ։