Sobes.tech
Middle+

Č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:

  1. Vykoná sa jeden dotaz na získanie všetkých Author.
  2. Potom v cykle pre každého Author sa vykoná samostatný dotaz na načítanie kolekcie books. Ak máme 100 autorov, bude vykonaných 100 ďalších dotazov na tabuľku Book. Celkovo 1 (pre autorov) + 100 (pre knihy) = 101 dotazov.

Riešenia problému N+1:

  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 DISTINCT sa používa na zabránenie duplikácie riadkov vo výsledku dotazu, ktorá môže nastať pri spojení jeden-ku-mnohým.

  2. Zmena typu načítania na EAGER: Zmena fetch = FetchType.LAZY (predvolené pre kolekcie) na fetch = 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ý.

  3. Použitie FetchMode v 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();
    
  4. Použitie BatchSize anotá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 books pre prvého autora, Hibernate načíta books naraz 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.

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