Co je problém N+1 v Hibernate a jak ho lze vyřešit?
sobes.tech AI
Odpověď od AI
Problém N+1 v Hibernate nastává při vybírání dat, kdy se pro získání rodičovských objektů provede jeden dotaz, a poté se pro každý podřízený objekt (nebo kolekci podřízených objektů) provede samostatný dotaz. To vede k N+1 dotazům do databáze, kde N je počet rodičovských objektů, místo optimálního jednoho dotazu nebo několika málo dotazů s joiny.
Příklad scénáře s problémem N+1:
Předpokládejme, že máme třídy Author a Book, kde autor může mít mnoho knih.
// Entita Author
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // LAZY fetch je výchozí pro kolekce
private List<Book> books;
// gettery a settery
}
// Entita Book
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
private Author author;
// gettery a settery
}
Chceme-li zobrazit všechny autory a názvy jejich knih:
Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 dotaz na autory
for (Author author : authors) {
System.out.println("Author: " + author.getName());
for (Book book : author.getBooks()) { // N dotazů na knihy (jeden na každého autora)
System.out.println("- Book: " + book.getTitle());
}
}
session.close();
V tomto příkladu:
- Proběhne jeden dotaz na získání všech
Author. - Poté v cyklu pro každého
Authorproběhne samostatný dotaz na načtení kolekcebooks. Pokud máme 100 autorů, bude provedeno 100 dalších dotazů na tabulkuBook. Celkem 1 (pro autory) + 100 (pro knihy) = 101 dotazů.
Řešení problému N+1:
-
Použití JOIN FETCH v JPQL/HQL: Explicitně načte související entity v jednom dotazu.
Session session = sessionFactory.openSession(); List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // Načte Author a jejich Books v jednom dotazu for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();Operátor
DISTINCTse používá k zabránění duplicitních řádků ve výsledku dotazu, které mohou vzniknout při spojení one-to-many. -
Změna typu načítání na EAGER: Změna
fetch = FetchType.LAZY(výchozí pro kolekce) nafetch = FetchType.EAGER.@Entity public class Author { // ... další pole @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // EAGER fetch private List<Book> books; // ... gettery a settery }Nedoporučuje se pro kolekce nebo entity s velkým množstvím vztahů, protože může vést k načítání nadbytečných dat a problémům s výkonem.
-
Použití
FetchModev Criteria API: Umožňuje určit, jak mají být načítány související entity.Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Author.class) .setFetchMode("books", FetchMode.JOIN); // Používá LEFT OUTER JOIN pro načítání knih 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žití
BatchSizeanotace: Určuje, že Hibernate načítá související objekty (nebo kolekce) ve skupinách určité velikosti, což snižuje počet dotazů, i když je to nevede k jednomu.@Entity @BatchSize(size = 10) // Hibernate bude načítat autory po skupinách po 10 public class Author { // ... další pole @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @BatchSize(size = 10) // Hibernate bude načítat knihy po skupinách po 10 private List<Book> books; // ... gettery a settery }Při procházení kolekce
booksprvního autora, Hibernate načtebooksi pro dalších 9 autorů (pokud byli načteni ve stejné relaci). To výrazně snižuje počet dotazů ve srovnání s N+1. -
Použití Entity Graphs: Umožňuje explicitně určit, které související objekty nebo kolekce mají být načteny při provádění dotazu.
@NamedEntityGraph(name = "author-with-books", attributeNodes = @NamedAttributeNode("books") ) @Entity public class Author { // ... pole a vztahy }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) // nebo fetchgraph podle požadovaného chování .getResultList(); for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();
Volba konkrétního řešení závisí na kontextu, vztahu one-to-one / one-to-many / many-to-many, objemu dat a požadované flexibilitě. JOIN FETCH a Entity Graphs jsou často preferovány pro načítání všech souvisejících dat v jednom dotazu, zatímco BatchSize je efektivní při práci s velkým množstvím entit a může být užitečný, když JOIN FETCH vede k příliš velkým výsledkům. EAGER načítání by mělo být používáno opatrně.