Τι είναι το πρόβλημα N+1 στο Hibernate και πώς μπορεί να λυθεί;
sobes.tech AI
Απάντηση από AI
Το πρόβλημα N+1 στο Hibernate προκύπτει κατά την επιλογή δεδομένων, όταν γίνεται ένα ερώτημα για την απόκτηση των γονικών αντικειμένων, και στη συνέχεια για κάθε παιδικό αντικείμενο (ή συλλογή παιδικών αντικειμένων) εκτελείται ένα ξεχωριστό ερώτημα. Αυτό οδηγεί σε N+1 ερωτήματα στη βάση δεδομένων, όπου N είναι ο αριθμός των γονικών αντικειμένων, αντί για ένα βελτιστοποιημένο ερώτημα ή λίγα ερωτήματα με ενώσεις.
Παράδειγμα σεναρίου με το πρόβλημα N+1:
Ας υποθέσουμε ότι υπάρχουν οι κλάσεις Author και Book, όπου ένας συγγραφέας μπορεί να έχει πολλά βιβλία.
// Οντότητα Author
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // LAZY fetch από προεπιλογή για συλλογές
private List<Book> books;
// getters και setters
}
// Οντότητα Book
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
private Author author;
// getters και setters
}
Αν θέλουμε να εμφανίσουμε όλους τους συγγραφείς και τα ονόματα των βιβλίων τους:
Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 ερώτημα για τους Author
for (Author author : authors) {
System.out.println("Author: " + author.getName());
for (Book book : author.getBooks()) { // N ερωτήματα για Book (ένα για κάθε συγγραφέα)
System.out.println("- Book: " + book.getTitle());
}
}
session.close();
Σε αυτό το παράδειγμα:
- Εκτελείται ένα ερώτημα για την απόκτηση όλων των
Author. - Στη συνέχεια, σε κάθε βρόχο, εκτελείται ένα ξεχωριστό ερώτημα για φόρτωση της συλλογής
books. Αν έχουμε 100 συγγραφείς, θα εκτελεστούν 100 επιπλέον ερωτήματα στη βάση δεδομένων για τα βιβλία. Συνολικά 1 (για τους συγγραφείς) + 100 (για τα βιβλία) = 101 ερωτήματα.
Λύσεις για το πρόβλημα N+1:
-
Χρήση JOIN FETCH σε JPQL/HQL: Φορτώνει ρητά τις σχετιζόμενες οντότητες σε ένα ερώτημα.
Session session = sessionFactory.openSession(); List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // Φορτώνει Author και τα Books τους σε ένα ερώτημα for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();Η εντολή
DISTINCTχρησιμοποιείται για την αποτροπή διπλότυπων γραμμών στο αποτέλεσμα του ερωτήματος. -
Αλλαγή του τύπου επιλογής σε EAGER: Αλλαγή του
fetch = FetchType.LAZY(προεπιλογή για συλλογές) σεfetch = FetchType.EAGER.@Entity public class Author { // ... άλλα πεδία @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // EAGER fetch private List<Book> books; // ... getters και setters }Δεν συνιστάται για συλλογές ή οντότητες με μεγάλο αριθμό σχέσεων, καθώς μπορεί να οδηγήσει σε φόρτωση περιττών δεδομένων και προβλήματα απόδοσης.
-
Χρήση
FetchModeστο Criteria API: Επιτρέπει τον καθορισμό του τρόπου φόρτωσης των σχετιζόμενων οντοτήτων.Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Author.class) .setFetchMode("books", FetchMode.JOIN); // Χρησιμοποιεί LEFT OUTER JOIN για φόρτωση των 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(); -
Χρήση
BatchSizeannotation: Καθορίζει στο Hibernate να φορτώνει τις σχετιζόμενες συλλογές σε ομάδες συγκεκριμένου μεγέθους, μειώνοντας τον αριθμό των ερωτημάτων.@Entity @BatchSize(size = 10) // Hibernate θα φορτώσει τους Author σε ομάδες των 10 public class Author { // ... άλλα πεδία @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @BatchSize(size = 10) // Hibernate θα φορτώσει τα books σε ομάδες των 10 private List<Book> books; // ... getters και setters }Όταν περιηγείστε στη συλλογή
booksγια τον πρώτο συγγραφέα, το Hibernate θα φορτώσει ταbooksκαι για τους επόμενους 9 συγγραφείς (αν έχουν φορτωθεί στην ίδια συνεδρία). Αυτό μειώνει σημαντικά τον αριθμό των ερωτημάτων. -
Χρήση Entity Graphs: Επιτρέπει τον ορισμό ρητά των σχετιζόμενων αντικειμένων ή συλλογών που πρέπει να φορτωθούν.
@NamedEntityGraph(name = "author-with-books", attributeNodes = @NamedAttributeNode("books") ) @Entity public class Author { // ... πεδία και σχέσεις }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) // ή fetchgraph, ανάλογα με την επιθυμητή συμπεριφορά .getResultList(); for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();
Η επιλογή της συγκεκριμένης λύσης εξαρτάται από το πλαίσιο, τη σχέση one-to-one / one-to-many / many-to-many, τον όγκο δεδομένων και την απαιτούμενη ευελιξία. Το JOIN FETCH και τα Entity Graphs συχνά προτιμώνται για την φόρτωση όλων των σχετιζόμενων δεδομένων σε ένα ερώτημα, ενώ το BatchSize είναι αποτελεσματικό όταν εργάζεστε με μεγάλο αριθμό οντοτήτων και μπορεί να είναι χρήσιμο όταν το JOIN FETCH οδηγεί σε πολύ μεγάλα αποτελέσματα. Η φόρτωση EAGER πρέπει να χρησιμοποιείται με προσοχή.