Sobes.tech
Middle+

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:

  1. Egy kérés történik az összes Author lekérésére.
  2. Ezután minden Author-hoz külön kérés történik a books kollekció betöltésére. Ha 100 szerző van, akkor 100 további kérés lesz a Book táblához. Összesen 1 (szerzők) + 100 (könyvek) = 101 kérés.

Az N+1 probléma megoldásai:

  1. 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 DISTINCT operátor megakadályozza a duplikált sorokat a lekérdezés eredményében.

  2. FetchType EAGER-re állítása: A fetch = FetchType.LAZY (alapértelmezett kollekciókhoz) helyett fetch = 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.

  3. FetchMode haszná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();
    
  4. BatchSize annotá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ő books kollekció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.

  5. 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.