Sobes.tech
Middle+

Kas ir N+1 problēma Hibernate un kā to var atrisināt?

sobes.tech AI

Atbilde no AI

Hibernate N+1 problēma rodas datu atlases laikā, kad tiek tiek vecāku objektu tiek iegūti ar vienu pieprasījumu, bet katram bērnu objektam (vai bērnu kolekcijai) tiek veikts atsevišķs pieprasījums. Tas noved pie N+1 pieprasījumiem uz datu bāzi, kur N ir vecāku objektu skaits, nevis optimāla viena pieprasījuma vai vairāku ar apvienojumiem.

N+1 problēmas piemērs:

Pieņemsim, ir klases Author un Book, kur autoram var būt daudz grāmatu.

// Autora vienība
@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // LAZY ielādes pēc noklusējuma
    private List<Book> books;

    // getters un setters
}

// Grāmatas vienība
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToOne
    private Author author;

    // getters un setters
}

Ja vēlamies izvadīt visus autorus un viņu grāmatu nosaukumus:

Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 pieprasījums autoru iegūšanai

for (Author author : authors) {
    System.out.println("Autors: " + author.getName());
    for (Book book : author.getBooks()) { // N pieprasījumi grāmatām
        System.out.println("- Grāmata: " + book.getTitle());
    }
}
session.close();

Šajā piemērā:

  1. Veic vienu pieprasījumu visiem Author iegūšanai.
  2. Tad ciklā katram Author tiek veikts atsevišķs pieprasījums books kolekcijas ielādei. Ja ir 100 autori, tiks veikti 100 papildu pieprasījumi Book kolekcijai. Kopā 1 (autoriem) + 100 (grāmatām) = 101 pieprasījumi.

N+1 problēmas risinājumi:

  1. JOIN FETCH izmantošana JPQL/HQL: skaidri ielādē saistītās entītijas vienā pieprasījumā.

    Session session = sessionFactory.openSession();
    List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // ielādē autoru un viņu grāmatas vienā pieprasījumā
    
    for (Author author : authors) {
        System.out.println("Autors: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Grāmata: " + book.getTitle());
        }
    }
    session.close();
    

    DISTINCT operators tiek izmantots, lai novērstu dublējošas rindas rezultātos, kas var rasties, apvienojot vienu-pret-mult.

  2. Mainīt ielādes tipu uz EAGER: mainīt fetch = FetchType.LAZY uz fetch = FetchType.EAGER.

    @Entity
    public class Author {
        // ... citi lauki
        @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // EAGER ielāde
        private List<Book> books;
        // ... getters un setters
    }
    

    Nav ieteicams kolekcijām vai entītijām ar daudzām saistībām, jo tas var novest pie pārmērīgas datu ielādes un veiktspējas problēmām ("dizaina produkts"). Der piemērot ManyToOne/OneToOne saistībām, kur sagaidāms, ka saistītais objekts vienmēr būs nepieciešams.

  3. FetchMode izmantošana Criteria API: ļauj norādīt, kā ir jāielādē saistītās entītijas.

    Session session = sessionFactory.openSession();
    Criteria criteria = session.createCriteria(Author.class)
        .setFetchMode("books", FetchMode.JOIN); // izmanto LEFT OUTER JOIN grāmatām
    
    List<Author> authors = criteria.list();
    
    for (Author author : authors) {
        System.out.println("Autors: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Grāmata: " + book.getTitle());
        }
    }
    session.close();
    
  4. BatchSize anotācija: norāda Hibernate ielādēt saistītos objektus (vai kolekcijas) grupās ar noteiktu izmēru, kas samazina pieprasījumu skaitu, bet nepadara tos vienu.

    @Entity
    @BatchSize(size = 10) // Hibernate ielādēs autorus pa 10
    public class Author {
        // ... citi lauki
        @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
        @BatchSize(size = 10) // Hibernate ielādēs grāmatas pa 10 autoru grupās
        private List<Book> books;
        // ... getters un setters
    }
    

    Kad jūs pāriesiet uz books kolekciju pirmajam autoram, Hibernate ielādēs books arī nākamajiem 9 autoru (ja tie tika ielādēti tajā pašā sesijā). Tas ievērojami samazina pieprasījumu skaitu salīdzinājumā ar N+1.

  5. Entity Graph izmantošana: ļauj skaidri definēt, kuri saistītie objekti vai kolekcijas ir jāielādē pie pieprasījuma.

    @NamedEntityGraph(name = "author-with-books",
        attributeNodes = @NamedAttributeNode("books")
    )
    @Entity
    public class Author {
        // ... lauki un saistības
    }
    
    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) // vai fetchgraph atkarībā no vēlamās uzvedības
        .getResultList();
    
    for (Author author : authors) {
        System.out.println("Autors: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Grāmata: " + book.getTitle());
        }
    }
    session.close();
    

Izvēle konkrētā risinājuma ir atkarīga no konteksta, vieno-pret-dažiem/viens-pret-dažiem/multivien-pret-multivien attiecībām, datu apjoma un nepieciešamās elastības. JOIN FETCH un Entity Graph bieži ir priekšroka, lai ielādētu visus saistītos datus vienā pieprasījumā, kamēr BatchSize ir efektīvs, strādājot ar lielu skaitu entītiju un var būt noderīgs, ja JOIN FETCH noved pie pārāk lieliem rezultātiem. EAGER ielāde jālieto uzmanīgi.