Mi az N+1 probléma a Hibernate-ben, és hogyan lehet megoldani?
sobes.tech MI
Válasz az MI-től
A Hibernate N+1 problémája akkor fordul elő, amikor adatokat választunk ki, és egy kérés végrehajtásával kapjuk meg a szülő objektumokat, majd minden egyes gyermek objektumhoz (vagy gyermekgyűjteményhez) külön kérés történik. Ez N+1 kéréshez vezet az adatbázisban, ahol N a szülő objektumok száma, szemben az optimális egy kérés vagy néhány összekapcsolt kérés használatával.
Példa N+1 problémára:
Tegyük fel, hogy az Author és Book osztályok vannak, ahol egy szerzőnek sok könyve lehet.
// Author entitás
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // LAZY fetch alapértelmezett kollekciókhoz
private List<Book> books;
// getterek és setterek
}
// Book entitás
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
private Author author;
// getterek és setterek
}
Ha az összes szerzőt és könyvük címét szeretnénk kiírni:
Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 kérés az Author-hoz
for (Author author : authors) {
System.out.println("Author: " + author.getName());
for (Book book : author.getBooks()) { // N kérés a Book-hoz (egy minden szerzőhöz)
System.out.println("- Book: " + book.getTitle());
}
}
session.close();
Ebben a példában:
- Egy kérés történik az összes
Authorlekérésére. - Ezután minden
Author-hoz külön kérés történik abookskollekció betöltésére. Ha 100 szerző van, akkor 100 további kérés lesz aBooktáblához. Összesen 1 (szerzők) + 100 (könyvek) = 101 kérés.
Az N+1 probléma megoldásai:
-
JOIN FETCH használata JPQL/HQL-ben: Egy kérésben betölti a kapcsolódó entitásokat.
Session session = sessionFactory.openSession(); List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // Betölti az Author-t és a könyveiket egy kérésben for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();A
DISTINCToperátor megakadályozza a duplikált sorokat a lekérdezés eredményében. -
FetchType EAGER-re állítása: A
fetch = FetchType.LAZY(alapértelmezett kollekciókhoz) helyettfetch = FetchType.EAGER.@Entity public class Author { // ... egyéb mezők @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // EAGER fetch private List<Book> books; // ... getterek és setterek }Nem javasolt kollekciók vagy sok kapcsolattal rendelkező entitások esetén, mert túlzott adatbetöltést és teljesítményproblémákat okozhat.
-
FetchModehasználata Criteria API-ban: Meghatározza, hogyan töltődjenek be a kapcsolódó entitások.Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Author.class) .setFetchMode("books", FetchMode.JOIN); // LEFT OUTER JOIN-t használ a könyvek betöltéséhez 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(); -
BatchSizeannotáció használata: Meghatározza, hogy a Hibernate csoportokban töltse be a kapcsolódó objektumokat (vagy kollekciókat), meghatározott méretben, csökkentve a kérdések számát, de nem csökkentve egyre.@Entity @BatchSize(size = 10) // Hibernate 10-es csoportokban tölt public class Author { // ... egyéb mezők @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @BatchSize(size = 10) // Hibernate 10-es csoportokban tölti be a könyveket private List<Book> books; // ... getterek és setterek }Amikor az első szerző
bookskollekcióját böngészi, a Hibernate az adott 10 szerzőbooks-át egyszerre tölti be. Ez jelentősen csökkenti a kérdések számát az N+1-hez képest. -
Entity Graphs használata: Lehetővé teszi, hogy explicit módon meghatározza, mely kapcsolódó objektumokat vagy kollekciókat kell betölteni a lekérdezés során.
@NamedEntityGraph(name = "author-with-books", attributeNodes = @NamedAttributeNode("books") ) @Entity public class Author { // ... mezők és kapcsolatok }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) // vagy fetchgraph a kívánt viselkedéstől függően .getResultList(); for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();
A konkrét megoldás kiválasztása a kontextustól, az egy-az-egyhez / egy-több / sok-több kapcsolat típusától, az adatmennyiségtől és a kívánt rugalmasságtól függ. A JOIN FETCH és az Entity Graphs gyakran az első választás, ha az összes kapcsolódó adatot egy kérésben akarjuk betölteni, míg a BatchSize hatékony nagy mennyiségű entitás esetén, különösen akkor, ha a JOIN FETCH túl nagy eredményt ad. Az EAGER betöltést óvatosan kell alkalmazni.