Đối với các ứng dụng sử dụng JPA/Hibernate thì các tác vụ liên quan đến truy vấn lấy dữ liệu từ database xảy ra rất thường xuyên. Đây là một điểm khá quan trọng mà Hibernate phải làm thật tốt để nâng cao hiệu năng cho ứng dụng. Khái niệm về FetchType được Hibernate đưa ra nhầm giải quyết việc lấy dữ liệu từ các bảng có mối quan hệ với nhau.
FetchType gồm 2 loại chính là Lazy và Eager đều có cách sử dụng khác nhau trong các trường hợp cụ thể. Chúng ta cùng tìm hiểu chúng ngay sau đây.
Default FetchType
Khi bạn mới bắt đầu với JPA – Hibernate, thông thường khi ánh xạ entity bạn sẽ không cần quan tâm đến FetchType vì các giá trị mặc định của nó thường phù hợp với các nhu cầu cơ bản. Trong trường hợp bạn không chỉ định FetchType thì giá trị mặc định thường là Lazy cho mối quan hệ many-to-many và many-to-one và Eager cho one-to-one.
Mặc dù các giá trị mặc định của FetchType thường phù hợp trong đa số trường hợp nhưng nếu muốn thay đổi chúng ta vẫn có thể thay đổi nếu muốn. Tất nhiên là thay đổi có chủ đích nhé, chứ đừng có làm bừa.
@Entity @Table(name = "PurchaseOrder") public class Order { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @OneToMany(mappedBy = "order", fetch = FetchType.EAGER) private Set<Item> items = new HashSet<Item>(); // getter, setter, constructor }
OK, giờ chúng ta sẽ tìm hiểu chi tiết xem mỗi FetchType có ý nghĩa gì trong JPA – Hibernate.
FetchType.Eager
Khi FetchType.Eager được sử dụng, Hibernate sẽ lấy tất cả các entity có quan hệ với root entity (entity được select trong câu truy vấn). Ví dụ mình có mối quan hệ many-to-one giữa Order và Item được ánh xạ như sau.
@Entity @Table(name = "PurchaseOrder") public class Order { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @OneToMany(mappedBy = "order", fetch = FetchType.EAGER) private Set<Item> items = new HashSet<Item>(); public Set<Item> getItems() { return items; } }
@Entity @Table(name = "Item") public class Item { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @ManyToOne private Order order; }
Khi mình lấy một dòng dữ liệu trong bảng Order có ID=1 từ database, Hibernate cũng sẽ lấy cả các Item có quan hệ many-to-one với nó.
Order order = em.find(Order.class, 1L);
Output
Hibernate: select order0_.id as id1_1_0_, items1_.order_id as order_id2_0_1_, items1_.id as id1_0_1_, items1_.id as id1_0_2_, items1_.order_id as order_id2_0_2_ from PurchaseOrder order0_ left outer join Item items1_ on order0_.id=items1_.order_id where order0_.id=?
Các bạn có thể thấy câu truy vấn được sinh ra bởi Hibernate thực hiện phép JOIN và SELECT các Item cùng với Order mà chúng thuộc về. Việc này có thể tốt trong thời gian đầu khi dữ liệu còn ít. Nhưng khi dữ liệu trở nên lớn hơn, việc lấy toàn bộ các Item như vậy có thể ảnh hưởng xấu đến hiệu xuất của chương trình, nhất là khi lấy mà không cần sử dụng đến.
FetchType.Lazy
Đây là FetchType mà Hibernate khuyên nên sử dụng, Hibernate chỉ lấy các entity có quan hệ với root entity khi cần thiết. Ví dụ mình thay đổi ánh xạ của Order thành FetchType.Lazy với Item.
@Entity @Table(name = "PurchaseOrder") public class Order { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @OneToMany(mappedBy = "order", fetch = FetchType.LAZY) private Set<Item> items = new HashSet<Item>(); public Set<Item> getItems() { return items; } }
Khi thực hiện lấy một dòng dữ liệu trong bảng Order với ID=1, thì Hibernate chỉ lấy dữ liệu trong bảng Order tương ứng mà không đụng đến các dữ liệu của các bảng có quan hệ.
Order order = em.find(Order.class, 1L);
Output
Hibernate: select order0_.id as id1_1_0_ from PurchaseOrder order0_ where order0_.id=?
FetchType.Lazy sẽ không ảnh hưởng đến logic code của bạn, chúng ta hoàn toàn có thể lấy những Item thuộc về Order thông qua getter() method, lúc này Hibernate truy vấn để lấy dữ liệu từ database thay vì lấy cùng lúc với Order như Eager .
Order order = em.find(Order.class, 1L); Set<Item> items = order.getItems(); System.out.println(items.size());
Output
Hibernate: select order0_.id as id1_1_0_ from PurchaseOrder order0_ where order0_.id=? Hibernate: select items0_.order_id as order_id2_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.order_id as order_id2_0_1_ from Item items0_ where items0_.order_id=?
Như vậy chúng ta thấy rằng Hibernate đã sinh ra 2 câu truy vấn thay vì 1 như chúng ta sử dụng FetchType.Eager. Việc sử dụng Lazy trường hợp này có thể không có vấn đề gì vì chỉ chúng ta chỉ có một Order cần lấy các Item hoặc thậm chí là một tập các Order có kích thước nhỏ.
Nhưng nếu bạn có có một danh sách số lượng lớn các Order được lấy lên từ database với FetchType.Lazy, mỗi phần tử bạn duyệt qua và lấy danh sách các Item thì Hibernate sẽ thực thi hàng loạt câu lệnh SELECT để lấy chúng, nó còn được gọi là N +1 Problem trong ORM.
Chúng ta có 2 cách để tránh xảy ra N + 1 Problem:
- Bạn có thể sử dụng FetchType.Eager trong trường hợp chắc chắn rằng bất cứ khi nào truy vấn Order entity thì bạn cũng cần lấy các Item entity để xử lý.
- Nếu có những trường hợp chỉ cần sử dụng Order entity mà không cần Item thì sử dụng FetchType.Lazy và sử dụng một các tùy chọn Hibernate cung cấp (FetchJoin, Named Entity Graph etc) khi cần thiết các Item của Order cùng lúc để xử lý.
Tóm lược
Qua bài viết này, chúng ta cần hiểu và sử dụng đúng loại FetchType cho các trường hợp cụ thể để tránh các lỗi về hiệu xuất phổ biến trong Hibernate. Trong đa số trường hợp thì FetchType.Lazy là một lựa chọn tốt, thế nhưng cần chú ý để tránh N + 1 problem.
Sử dụng FetchType.Eager khi chắc chắn rằng bạn muốn lấy tất cả các entity liên quan bất cứ khi nào lấy root entity. Nhưng trong thực tế thường rất ít các trường hợp như vậy, nếu sử dụng Eager sẽ lãng phí khá nhiều tài nguyên.
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://thorben-janssen.com/entity-mappings-introduction-jpa-fetchtypes/