Čo je problém N+1 v Hibernate a ako ho možno vyriešiť?
sobes.tech AI
Odpoveď od AI
Problém N+1 v Hibernate nastáva pri výbere dát, keď sa na získanie rodičovských objektov vykoná jeden dotaz, a potom pre každý detský objekt (alebo kolekciu detských objektov) sa vykoná samostatný dotaz. To vedie k N+1 dotazom do databázy, kde N je počet rodičovských objektov, namiesto optimálneho jedného alebo niekoľkých dotazov s spojeniami.
Príklad scenára s problémom N+1:
Predstavme si, že máme triedy Author a Book, kde autor môže mať veľa kníh.
// 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 predvolené pre kolekcie
private List<Book> books;
// getters a setters
}
// Book entity
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
private Author author;
// getters a setters
}
Ak chceme vypísať všetkých autorov a názvy ich kníh:
Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 dotaz na Author
for (Author author : authors) {
System.out.println("Author: " + author.getName());
for (Book book : author.getBooks()) { // N dotazov na Book (po jednom na každého autora)
System.out.println("- Book: " + book.getTitle());
}
}
session.close();
V tomto príklade:
- Vykoná sa jeden dotaz na získanie všetkých
Author. - Potom v cykle pre každého
Authorsa vykoná samostatný dotaz na načítanie kolekciebooks. Ak máme 100 autorov, bude vykonaných 100 ďalších dotazov na tabuľkuBook. Celkovo 1 (pre autorov) + 100 (pre knihy) = 101 dotazov.
Riešenia problému N+1:
-
Použitím JOIN FETCH v JPQL/HQL: Jasne načíta prepojené entity za jeden dotaz.
Session session = sessionFactory.openSession(); List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // Načíta Author a ich Books za jeden dotaz for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();Príkaz
DISTINCTsa používa na zabránenie duplikácie riadkov vo výsledku dotazu, ktorá môže nastať pri spojení jeden-ku-mnohým. -
Zmena typu načítania na EAGER: Zmena
fetch = FetchType.LAZY(predvolené pre kolekcie) nafetch = FetchType.EAGER.@Entity public class Author { // ... ostatné polia @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // EAGER fetch private List<Book> books; // ... getters a setters }Neodporúča sa pre kolekcie alebo entity s veľkým počtom väzieb, pretože môže viesť k načítaniu nadbytočných dát a problémom s výkonom (efekt "kartézskeho súčinu"). Vhodné pre väzby ManyToOne/OneToOne, kde sa očakáva, že prepojený objekt bude vždy potrebný.
-
Použitie
FetchModev Criteria API: Umožňuje určiť, ako sa majú načítavať prepojené entity.Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Author.class) .setFetchMode("books", FetchMode.JOIN); // Používa LEFT OUTER JOIN na načítanie 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(); -
Použitie
BatchSizeanotácie: Určuje Hibernate, aby načítaval prepojené objekty (alebo kolekcie) skupinovo v určitej veľkosti, čím znižuje počet dotazov, hoci ich nezháňa do jedného.@Entity @BatchSize(size = 10) // Hibernate bude načítavať Author po skupinách po 10 public class Author { // ... ostatné polia @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @BatchSize(size = 10) // Hibernate bude načítavať books pre skupiny Author po 10 private List<Book> books; // ... getters a setters }Pri prechádzaní kolekcie
bookspre prvého autora, Hibernate načítabooksnaraz pre ďalších 9 autorov (ak boli načítaní v rovnakej relácii). To výrazne znižuje počet dotazov v porovnaní s N+1. -
Použitie Entity Graphs: Umožňuje jasne definovať, ktoré prepojené objekty alebo kolekcie majú byť načítané pri vykonávaní dotazu.
@NamedEntityGraph(name = "author-with-books", attributeNodes = @NamedAttributeNode("books") ) @Entity public class Author { // ... polia a vzťahy }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) // alebo fetchgraph podľa požadovaného správania .getResultList(); for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();
Výber konkrétneho riešenia závisí od kontextu, vzťahu jeden-ku-jednému/jeden-ku-mnohým/mnohé-ku-mnohým, objemu dát a požadovanej flexibility. JOIN FETCH a Entity Graphs sú často preferované na načítanie všetkých prepojených dát v jednom dotaze, zatiaľ čo BatchSize je efektívny pri práci s veľkým množstvom entít a môže byť užitočný, keď JOIN FETCH vedie k príliš veľkým výsledkom. EAGER načítanie by sa malo používať opatrne.