Ի՞նչ է 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();
Այս օրինակով՝
- Կատարվում է մեկ հարցում՝ բոլոր
Author-ների համար: - Յուրաքանչյուր
Author-ի համար կատարվում է առանձին հարցում՝booksհավաքածուի բեռնում:
Եթե ունենք 100 հեղինակ, ապա կատարվելու է 100 լրացուցիչ հարցում՝ Book-ների համար: Ընդհանուր՝ 1 (հեղինակների համար) + 100 (գրքերի համար) = 101 հարցում:
Նպատակը՝ լուծել N+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հրամանը օգտագործվում է կրկնվող տողերի կանխարգելման համար՝ միացումների արդյունքում: -
Փոխել բեռնումը 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է, և կապակցված օբյեկտը միշտ անհրաժեշտ է:
-
Օգտագործել
FetchModeCriteria 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(); -
Օգտագործել
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-ի:
- Երբ անցնում եք
-
Օգտագործել 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 բեռնումը պետք է օգտագործել զգուշությամբ։