Phân biệt FetchMode và FetchType trong JPA/Hibernate

Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu FetchMode là gì? và nó khác biệt như thế nào so với FetchType?

Về cơ bản thì FetchMode là một tính năng Hibernate cung cấp để tối ưu hoá hiệu năng truy vấn dữ liệu. Thoạt nhìn khá giống với với các chức năng của FetchType nên có nhiều bạn nhầm lẫn cách sử dụng, chúng ta sẽ cùng xem ví dụ sau bắt đầu với ánh xạ entity

Chúng ta có một bảng Customer có quan hệ one-to-many với Order với 2 thuộc tính cơ bản

@Entity
@Table(name = "customers")
public class Customer {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "customer")
    @Fetch(value = FetchMode.SELECT)
    private Set<Order> orders = new HashSet<>();

    // getters and setters
}

Và Order cũng chỉ có các thuộc tính cơ bản được ánh xạ như sau

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    // getters and setters
}

Sau khi setup xong ví dụ trên chúng ta sẽ tìm hiểu từng kiểu trong FetchMode như FetchMode.JOIN, FetchMode.SUBSELECT, FetchMode.SELECT.

FetchMode.SELECT

Các bạn hãy để ý rằng khi ánh xạ Customer thì mình đã chú thích thuộc tính orders với @Fetch annotation

@OneToMany
@Fetch(FetchMode.SELECT)
private Set<Orders> orders;

@Fetch annotation dùng để mô tả cách mà Hibernate lấy orders khi nó truy vấn một Customer.

FetchMode.SELECT chỉ ra rằng Hibernate nên tải danh sách các orders theo hướng lazy. Nghĩa là khi chúng ta truy vấn một dòng dữ liệu trong Customer thì Hibernate chỉ lấy chính xác nó mà không tải thêm các orders cùng lúc.

Cho đến khi chúng ta cần sử dụng các orders này thông qua getter method, Hibernate sẽ lại thực thi một câu truy vấn để lấy danh sách orders từ database lên.

List<Customer> customers = em.createQuery("select c from Customer c").getResultList();
for (Customer customer : customers) {
    System.out.println(customer.getOrders().size());
}

Chúng ta sẽ thấy Hibernate sinh ra 1 câu truy vấn lấy danh sách các CustomerN câu truy vấn tùy thuộc vào số lượng dữ liệu Customer chúng ta có để lấy danh sách các Order tương ứng với từng Customer.

// 1 câu truy vấn để lấy danh sách Customer
Hibernate: 
    /* select
        c 
    from
        Customer c */ select
            customer0_.id as id1_0_ 
        from
            customers customer0_
// 1 câu truy vấn để lấy Order 
Hibernate: 
    select
        orders0_.customer_id as customer3_1_0_,
        orders0_.id as id1_1_0_,
        orders0_.id as id1_1_1_,
        orders0_.customer_id as customer3_1_1_,
        orders0_.name as name2_1_1_ 
    from
        orders orders0_ 
    where
        orders0_.customer_id=?
// Thêm 1 câu truy vấn để lấy Order 
Hibernate: 
    select
        orders0_.customer_id as customer3_1_0_,
        orders0_.id as id1_1_0_,
        orders0_.id as id1_1_1_,
        orders0_.customer_id as customer3_1_1_,
        orders0_.name as name2_1_1_ 
    from
        orders orders0_ 
    where
        orders0_.customer_id=?
// N câu câu truy vấn Order

Việc thực thi nhiều câu truy vấn như vậy sẽ làm giảm hiệu năng đáng kể của ứng dụng, nó còn được gọi là N + 1 Problem phổ biến trong trong các ORM framework và Hibernate.

@BatchSize

Để cải thiện hiệu suất của FetchMode.SELECT Hibernate còn cung cấp một tùy chọn khác cho phép gom nhóm một số lượng câu SELECT thông qua @BatchSize annotation để thực thi một lần thay vì thực thi riêng lẽ như thông thường.

@BatchSize annotation nhận vào một giá trị số nguyên là số lượng câu truy vấn được gom nhóm

Giả sử trong database mình có 5 record Customer, nếu mặc định thì FetchMode.SELECT sẽ thực thi thêm 5 câu truy vấn để lấy các Order, Khi mình để @BatchSize(size=10) thì Hibernate sẽ thực thi một câu SELECT duy nhất

@OneToMany(mappedBy = "customer")
@Fetch(value = FetchMode.SELECT)
@BatchSize(size=10)
private Set<Order> orders = new HashSet<>();

FetchMode.JOIN

Trái ngược với FetchMode.SELECT tải lazy, FetchMode.JOIN sẽ tải theo chiến lược eager, nghĩa là các orders collection sẽ được tải lên cùng lúc với Customer, điều này sẽ giúp chúng ta tránh được N + 1 Problem nhưng nếu trong một logic không cần sử dụng đến orders việc dùng FetchMode.JOIN sẽ khiến dữ liệu bị dư thừa.

@OneToMany
@Fetch(FetchMode.JOIN)
private Set<Orders> orders;

Khi chúng ta truy vấn Customer thì các Order quan hệ với chúng cũng sẽ được tải lên cùng lúc.

List<Customer> customers = em.createQuery("select c from Customer c").getResultList();
Hibernate: 
    /* select
        c 
    from
        Customer c */ select
            customer0_.id as id1_0_ 
        from
            customers customer0_
Hibernate: 
    /* load one-to-many entities.Customer.orders */ select
        orders0_.customer_id as customer3_1_1_,
        orders0_.id as id1_1_1_,
        orders0_.id as id1_1_0_,
        orders0_.customer_id as customer3_1_0_,
        orders0_.name as name2_1_0_ 
    from
        orders orders0_ 
    where
        orders0_.customer_id in (
            ?, ?
        )
Oct 14, 2020 10:59:25 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH000030: Cleaning up connection pool [jdbc:mysql://localhost:3306/fetchmode?useSSL=false&serverTimezone=UTC]
Disconnected from the target VM, address: '127.0.0.1:51142', transport: 'socket'

Process finished with exit code 0

FetchMode.SUBSELECT

Một cách để giảm thiểu số lượng truy vấn trong FetchMode.SELECT là sử dụng thêm @BatchSize để gom nhóm các câu truy vấn. Thế nhưng một câu hỏi rất lớn được đặt ra làm BatchSize với số lượng bao nhiêu là đủ? nó không thể nào bao quát tất cả các trường hợp với các dữ liệu trong database.

FetchMode.SUBSELECT sinh ra để giải quyết vấn đề trên cho phép tải các tập dữ liệu ở chiều many trong one-to-many với một câu truy vấn con thay vì thực thi hàng loạt câu SELECT như FetchMode.SELECT.

@Fetch(value = FetchMode.SUBSELECT)
private Set<Order> orders = new HashSet<>();

Khi thực thi đoạn code sau

List<Customer> customers = em.createQuery("select c from Customer c").getResultList();
for (Customer customer : customers) {
    System.out.println(customer.getOrders().size());
}

Thì kết quả chúng ta có thể thấy  được Hibernate chỉ sinh thêm đúng 1 câu truy vấn duy nhất để lấy tất cả các Order liên quan đến tập dữ liệu Customer được lấy lên trước đó.

Hibernate: 
    /* select
        c 
    from
        Customer c */ select
            customer0_.id as id1_0_ 
        from
            customers customer0_
Hibernate: 
    /* load one-to-many entities.Customer.orders */ select
        orders0_.customer_id as customer3_1_1_,
        orders0_.id as id1_1_1_,
        orders0_.id as id1_1_0_,
        orders0_.customer_id as customer3_1_0_,
        orders0_.name as name2_1_0_ 
    from
        orders orders0_ 
    where
        orders0_.customer_id in (
            select
                customer0_.id 
            from
                customers customer0_
        )

Như vậy chúng ta có thể thấy rằng FetchMode.SUBSELECT nên được sử dụng trong one-to-many hơn FetchMode.SELECT.

Điểm khác nhau giữa FetchMode và FetchType

Về cơ bản, FetchMode định nghĩa cách Hibernate sẽ lấy dữ liệu (select, join, subselect), trong khi FetchType định nghĩa dữ liệu sẽ được tải lên theo hướng lazy hay eager.

Việc sử dụng FetchModeFetchType cùng lúc đôi lúc xảy ra các xung đột, vì thế sẽ có một số chuẩn chung mà Hibernate áp dụng để xử lý như sau: 

  • Nếu không chỉ định FetchMode trong entity FetchMode.JOIN sẽ được sử dụng mặc định và FetchType lúc này sẽ hoạt động như đúng những gì chúng được định nghĩa.
  • Với FetchMode.SELECT hoặc FetchMode.SUBSELECT được đặt, FetchType vẫn sẽ hoạt động bình thường.
  • Với FetchMode.JOIN được chỉ định, FetchType sẽ bị bỏ qua và các câu truy vấn sẽ luôn Eager.

Tóm lược

Như vậy giữa FetchType và FetchMode có mối tương quan lẫn nhau, khi FetchMode.JOIN được chỉ định, FetchType sẽ bị bỏ qua và truy vấn lấy dữ liệu luôn là Eager, đây là một điểm quan trọng hết sức cần lưu ý. 

Khi không chỉ định FetchMode thì mặc định nnó vẫn là JOIN thế nhưng nó cho phép FetchType hoặc định bình thường, trong trường hợp FetchType.Lazy thì xem như FetchMode.JOIN bị loại bỏ.

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://www.baeldung.com/hibernate-fetchmode

https://www.solidsyntax.be/2013/10/17/fetching-collections-hibernate/

http://www.christophbrill.de/en/posts/hibernate-fetch-subselect-performance/

5 1 vote
Article Rating
Subscribe
Notify of
guest
1 Comment
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
1
0
Would love your thoughts, please comment.x
()
x