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ā:
- Veic vienu pieprasījumu visiem
Authoriegūšanai. - Tad ciklā katram
Authortiek veikts atsevišķs pieprasījumsbookskolekcijas ielādei. Ja ir 100 autori, tiks veikti 100 papildu pieprasījumiBookkolekcijai. Kopā 1 (autoriem) + 100 (grāmatām) = 101 pieprasījumi.
N+1 problēmas risinājumi:
-
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();DISTINCToperators tiek izmantots, lai novērstu dublējošas rindas rezultātos, kas var rasties, apvienojot vienu-pret-mult. -
Mainīt ielādes tipu uz EAGER: mainīt
fetch = FetchType.LAZYuzfetch = 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.
-
FetchModeizmantoš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(); -
BatchSizeanotā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
bookskolekciju pirmajam autoram, Hibernate ielādēsbooksarī 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. -
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.