Sobes.tech
Middle+

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:

  1. Proběhne jeden dotaz na získání všech Author.
  2. Poté v cyklu pro každého Author proběhne samostatný dotaz na načtení kolekce books. Pokud máme 100 autorů, bude provedeno 100 dalších dotazů na tabulku Book. Celkem 1 (pro autory) + 100 (pro knihy) = 101 dotazů.

Řešení problému N+1:

  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 DISTINCT se používá k zabránění duplicitních řádků ve výsledku dotazu, které mohou vzniknout při spojení one-to-many.

  2. Změna typu načítání na EAGER: Změna fetch = FetchType.LAZY (výchozí pro kolekce) na fetch = 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.

  3. Použití FetchMode v 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();
    
  4. Použití BatchSize anotace: 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 books prvního autora, Hibernate načte books i 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.

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