N + 1 Problem trong trong JPA và Hibernate

Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu N + 1 Problem là gì khi sử dụng JPA và Hibernate và đâu là cách tốt nhất để giải quyết vấn đề này. Xin lưu ý rằng N +1 Problem không chỉ giành riêng cho Hibernate mà chúng ta vẫn có thể gặp chúng ở các ORM framework khác.

N + 1 Problem là gì?

Giả sử mình ánh xạ 2 entity Post và PostComment có mối quan hệ many-to-one với FetchType.Lazy.

n+1-1

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Column
    private String title;

    @OneToMany(mappedBy = "post")
    private Set<PostComment> postComments = new HashSet<>();

    public Set<PostComment> getPostComments() {
        return postComments;
    }
    // ... getter, setter, constructor method.
}
@Entity
public class PostComment {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @ManyToOne
    private Post post;

    @Column
    private String comment;

    // ... getter, setter, constructor method.
}

Chúng ta sẽ tiến hành thêm dữ liệu vào database cho bảng Post.

INSERT INTO Post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)
  
INSERT INTO Post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)
  
INSERT INTO Post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)
  
INSERT INTO Post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)

Và PostComment

INSERT INTO PostComment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)
  
INSERT INTO PostComment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)
  
INSERT INTO PostComment (post_id, review, id)
VALUES (3, 'Five Stars', 3)
  
INSERT INTO PostComment (post_id, review, id)
VALUES (4, 'A great reference book', 4)

FetchType.Lazy

Khi ánh xạ mối quan hệ với FetchType.Lazy, các dữ liệu liên quan sẽ được tải lên khi cần thiết. Ví dụ các PostComment entity quan hệ với Post chỉ được tải lên khi bạn gọi getPostComments().

List<Post> posts = em.createQuery("select p from Post p").getResultList();
for (Post post : posts) {
     System.out.println("Post Comments: " + post.getPostComments().size());
}

Output

Hibernate: /* select p from Post p */ select post0_.id as id1_0_, post0_.title as title2_0_ from Post post0_
// SQL SELECT POST_COMMENT
Hibernate: select postcommen0_.post_id as post_id3_1_0_, postcommen0_.id as id1_1_0_, postcommen0_.id as id1_1_1_, postcommen0_.comment as comment2_1_1_, postcommen0_.post_id as post_id3_1_1_ from PostComment postcommen0_ where postcommen0_.post_id=?
// SQL SELECT POST_COMMENT
Hibernate: select postcommen0_.post_id as post_id3_1_0_, postcommen0_.id as id1_1_0_, postcommen0_.id as id1_1_1_, postcommen0_.comment as comment2_1_1_, postcommen0_.post_id as post_id3_1_1_ from PostComment postcommen0_ where postcommen0_.post_id=?
// SQL SELECT POST_COMMENT
Hibernate: select postcommen0_.post_id as post_id3_1_0_, postcommen0_.id as id1_1_0_, postcommen0_.id as id1_1_1_, postcommen0_.comment as comment2_1_1_, postcommen0_.post_id as post_id3_1_1_ from PostComment postcommen0_ where postcommen0_.post_id=?
// SQL SELECT POST_COMMENT
Hibernate: select postcommen0_.post_id as post_id3_1_0_, postcommen0_.id as id1_1_0_, postcommen0_.id as id1_1_1_, postcommen0_.comment as comment2_1_1_, postcommen0_.post_id as post_id3_1_1_ from PostComment postcommen0_ where postcommen0_.post_id=?

Khi thực thi đoạn code trên chúng ta thấy rằng Hibernate sẽ thực thi một câu SELECT để lấy tất cả các Post trong database. Với mỗi lần lặp qua từng Post để lấy PostComment Hibernate sẽ thực thi một câu SELECT xuống database để lấy các PostCommemt.

Tổng kết lại chúng ta có 1 câu SELECT để lấy Post và N câu SELECT để lấy các PostComment. Rõ ràng đây là một chiến lược kém hiệu quả khi có quá nhiều câu truy vấn được gửi xuống database hay còn được gọi là N + 1 Problem.

Nếu bạn muốn tải các PostComment cùng lúc với Post thì có thể sử dụng JOIN FETCH để tránh N + 1 Problem

List<Post> posts = em.createQuery("select p from Post p join fetch p.postComments pc").getResultList();

Output

Hibernate: select post0_.id as id1_0_0_, postcommen1_.id as id1_1_1_, post0_.title as title2_0_0_, postcommen1_.comment as comment2_1_1_, postcommen1_.post_id as post_id3_1_1_, postcommen1_.post_id as post_id3_1_0__, postcommen1_.id as id1_1_0__ from Post post0_ 
inner join PostComment postcommen1_ on post0_.id=postcommen1_.post_id

FetchType.Eager

Ánh xạ FetchType.Eager rõ ràng không phải là ý tưởng hay khi dữ liệu sẽ luôn tải lên mặc cho bạn có cần sử dụng đến chúng hay không. Hơn nữa sử dụng FetchType.Eager cũng có khả năng sinh ra N + 1 Problem.

@Entity
public class PostComment {
    @ManyToOne
    private Post post;
    // ...
}

Không may, @ManyToOne@OneToOne mặc định là FetchType.Eager, khi bạn ánh xạ quan hệ PostComment với Post như trên, khi bạn truy vấn mà quên sử dụng FETCH JOIN khi lấy PostCommemt.

 List<PostComment> postComments = em.createQuery("select p from PostComment p").getResultList();

Thì chúng ta cũng sẽ bị N + 1 Problem.

Hibernate: /* select p from PostComment p */ select postcommen0_.id as id1_1_, postcommen0_.comment as comment2_1_, postcommen0_.post_id as post_id3_1_ from PostComment postcommen0_
// SELECT POST 1
Hibernate: select post0_.id as id1_0_0_, post0_.title as title2_0_0_ from Post post0_ where post0_.id=?
// SELECT POST 2
Hibernate: select post0_.id as id1_0_0_, post0_.title as title2_0_0_ from Post post0_ where post0_.id=?
// SELECT POST 3
Hibernate: select post0_.id as id1_0_0_, post0_.title as title2_0_0_ from Post post0_ where post0_.id=?
// SELECT POST 4
Hibernate: select post0_.id as id1_0_0_, post0_.title as title2_0_0_ from Post post0_ where post0_.id=?

Một lần nữa chúng ta sẽ sử dụng JOIN FETCH để giải quyết vấn đề.

List<PostComment> postComments = em.createQuery("select pc from PostComment pc join fetch pc.post").getResultList();

Output

select postcommen0_.id as id1_1_0_, post1_.id as id1_0_1_, postcommen0_.comment as comment2_1_0_, postcommen0_.post_id as post_id3_1_0_, post1_.title as title2_0_1_ from PostComment postcommen0_ 
inner join Post post1_ on postcommen0_.post_id=post1_.id

Tóm lược

Nhận biết N + 1 problem khi làm việc với các ORM là điều hết sức quan trọng không chỉ riêng JPA – Hibernate. Đây có lẽ là một lỗi mà người mới tiếp xúc với các ORM thường gặp phải, thậm chí những người làm lâu năm cũng đôi khi vướng phải trong một số trường hợp khó nhận thấy hơn gây ảnh hưởng nghiêm trọng đến hiệu năng của chương trình.

Khi sử dụng JPA – Hibermate JOIN FETCH là một trong những cách giúp tránh N + 1 hiệu quả bậc nhất, linh hoạt hơn FetchType.Eager.

Sau cùng, các bạn có thể tham khảo mã nguồn được mình công khai trên github.

Nếu vẫn chưa biết cách cấu hình một project JPA – Hibermate thì bạn có thể tham khảo bài viết sau để thực hành.

Nguồn tham khảo

https://vladmihalcea.com/n-plus-1-query-problem/

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x