Bài 2: Giải Quyết Vấn Đề N+1 Select với Hibernate

·

4 min read

Mục tiêu: Hiểu rõ về vấn đề N+1 Select và cách giải quyết nó bằng cách sử dụng các kỹ thuật tối ưu hóa truy vấn trong Hibernate như JOIN FETCHEntityGraph.

1. Mô tả Vấn Đề N+1 Select

N+1 Select Problem là một vấn đề phổ biến trong ORM, xảy ra khi một truy vấn chính tải một danh sách các đối tượng (N đối tượng) và sau đó thực hiện N truy vấn phụ để tải dữ liệu liên quan cho từng đối tượng trong danh sách đó.

Ví dụ Minh Họa

Giả sử chúng ta có hai thực thể AuthorBook với mối quan hệ một-nhiều:

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

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books;
}

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
}

Khi chúng ta truy vấn danh sách các Author và muốn lấy cả danh sách các Book cho mỗi Author, một truy vấn không tối ưu có thể dẫn đến vấn đề N+1 Select:

List<Author> authors = entityManager.createQuery("SELECT a FROM Author a", Author.class).getResultList();
for (Author author : authors) {
    List<Book> books = author.getBooks(); // Truy vấn thêm N lần cho mỗi Author
}

2. Giải pháp cho Vấn Đề N+1 Select

Sử dụng JOIN FETCH

Một cách hiệu quả để giải quyết vấn đề này là sử dụng JOIN FETCH trong HQL (Hibernate Query Language) để tải đồng thời dữ liệu liên quan:

List<Author> authors = entityManager.createQuery(
    "SELECT a FROM Author a JOIN FETCH a.books", Author.class).getResultList();

Câu truy vấn này sẽ tải danh sách AuthorBook liên quan trong một truy vấn duy nhất, giúp tránh vấn đề N+1 Select.

Sử dụng EntityGraph

EntityGraph là một cách khác để chỉ định các thực thể liên quan cần tải trong cùng một truy vấn:

@EntityGraph(attributePaths = {"books"})
List<Author> findAll();

Khi sử dụng EntityGraph, Hibernate sẽ tự động tải các Book liên quan cho mỗi Author trong một truy vấn duy nhất.

3. Thực hành

Ví dụ Thực hành với JOIN FETCH

Dưới đây là một ví dụ cụ thể sử dụng JOIN FETCH:

public List<Author> findAllAuthorsWithBooks() {
    return entityManager.createQuery(
        "SELECT a FROM Author a JOIN FETCH a.books", Author.class).getResultList();
}
Ví dụ Thực hành với EntityGraph

Đầu tiên, định nghĩa một EntityGraph:

@Entity
@NamedEntityGraph(
    name = "author-books-graph",
    attributeNodes = @NamedAttributeNode("books")
)
public class Author {
    // ...
}

Sau đó, sử dụng EntityGraph trong truy vấn:

public List<Author> findAllAuthorsWithBooksUsingEntityGraph() {
    EntityGraph<?> entityGraph = entityManager.getEntityGraph("author-books-graph");
    return entityManager.createQuery("SELECT a FROM Author a", Author.class)
        .setHint("javax.persistence.fetchgraph", entityGraph)
        .getResultList();
}
So sánh Hiệu Suất Trước và Sau Khi Áp Dụng Giải Pháp

Để thấy rõ sự khác biệt về hiệu suất, bạn có thể sử dụng công cụ profiler hoặc log các truy vấn SQL trước và sau khi áp dụng các kỹ thuật tối ưu hóa:

Trước khi tối ưu hóa:

List<Author> authors = entityManager.createQuery("SELECT a FROM Author a", Author.class).getResultList();
for (Author author : authors) {
    List<Book> books = author.getBooks(); // N+1 Select Problem
}
  • Số lượng truy vấn SQL: N+1

  • Thời gian thực hiện: Cao

Sau khi tối ưu hóa bằng JOIN FETCH:

List<Author> authors = entityManager.createQuery(
    "SELECT a FROM Author a JOIN FETCH a.books", Author.class).getResultList();
  • Số lượng truy vấn SQL: 1

  • Thời gian thực hiện: Thấp

Sau khi tối ưu hóa bằng EntityGraph:

public List<Author> findAllAuthorsWithBooksUsingEntityGraph() {
    EntityGraph<?> entityGraph = entityManager.getEntityGraph("author-books-graph");
    return entityManager.createQuery("SELECT a FROM Author a", Author.class)
        .setHint("javax.persistence.fetchgraph", entityGraph)
        .getResultList();
}
  • Số lượng truy vấn SQL: 1

  • Thời gian thực hiện: Thấp

Kết luận

Vấn đề N+1 Select là một vấn đề phổ biến trong ORM nhưng có thể được giải quyết hiệu quả bằng cách sử dụng JOIN FETCH hoặc EntityGraph trong Hibernate. Áp dụng các kỹ thuật này không chỉ giúp tối ưu hóa hiệu suất mà còn cải thiện trải nghiệm người dùng bằng cách giảm thời gian tải dữ liệu. Hãy thử các giải pháp trên và kiểm tra sự khác biệt trong ứng dụng của bạn!