JPA-Hibernate giúp chúng ta đồng bộ hoá các quá trình chuyển đổi trạng thái của entity xuống database. Quá trình thay đổi trạng thái của các entity trong JPA-Hibernate có khả năng gây rảnh hưởng đến các entity có liên kết với chúng. Để giải quyết vấn đề này, Hibernate đã giới thiệu Cascase mà chúng ta sẽ tìm hiểu ngay sau đây.
Cascade là gì?
Ví dụ User và Order có quan hệ one-to-many, khi User bị xoá khỏi database việc các Order tồn tại riêng lẽ là không có ý nghĩa vì vậy chúng cần được xoá theo User.Nếu trước đây thì có lẽ chúng ta phải xoá chúng một cách thủ công, nhưng Cascade được Hibernate giới thiệu để giải quyết vấn đề này một cách tự động.
Như đã bàn luận ở trên thì Cascade là cơ chế cho phép lan truyền quá trình chuyển đổi từ một entity đến các related entity(các entity có mối quan hệ với một entity khác).
JPA Cascade Type
JPA hỗ trợ các kiểu tất cả gồm 6 kiểu được định nghĩa trong javax.persistence.CascadeType enum:
- ALL
- PERSIST
- MERGE
- REMOVE
- REFRESH
- DETACH
Hibernate Cascade Type
Ngoài các kiểu mà JPA hỗ trợ thì Hibernate còn hỗ trợ thêm 3 kiểu được định nghĩa trong org.hibernate.annotations.CascadeType enum:
- REPLICATE
- SAVE_UPDATE
- LOCK
Maven dependency
Để thực hành được bài này chúng ta cần sử những dependency sau trong project maven.
Nếu bạn chưa biết cách cấu hình JPA-Hibernate thì có thể xem qua bài hướng dẫn này hoặc có thể theo dõi để nắm ý chính rồi thực hành sau cũng được.
<dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>5.0.0.Final</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-java8</artifactId> <version>5.1.0.Final</version> </dependency> </dependencies>
Giả sử mình có mô hình 2 bảng dữ liệu gồm User, Order được ánh xạ như sau:
@Entity @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; private String name; @OneToMany(mappedBy = "user") private Set<Order> orders = new HashSet<>(); // getter, setter, construtor }
@Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; private LocalDate date; @ManyToOne(fetch = FetchType.LAZY) private User user; // // getter, setter, construtor }
Điểm khác nhau của các loại Cascade
Mỗi loại Cascade trong JPA và Hibernate đều có mục đích sử dụng riêng, mà trong phần này chúng ta cùng tìm hiểu từng loại một.
Cascade.PERSIST
Như bạn đã biết hàm JPA#Persist() chuyển một entity từ trạng thái transient(transient entity) sang persistent(persistent entity). Cascade.PERSIST sẽ lan truyền hoạt động này một cách tự động từ persistent entity sang các related entity. Ví dụ khi chúng ta lưu một User entity thì các Order entity liên kết với nó cũng được lưu theo.
Để kiểm tra, trước tiên mình thêm vào ánh xạ của User với CascadeType.PERSIST
@Entity @Table(name = "user") public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST) private Set<Order> orders = new HashSet<>(); // .... }
User user = new User(); Order order = new Order(); order.setUser(user); user.getOrders().add(order); em.persist(user);
Với Cascade.PERSIST chỉ cần chúng ta lưu User thì Order sẽ tự động lưu theo.
Hibernate: /* insert entities.User */ insert into user (name, id) values (?, ?) Hibernate: /* insert entities.Order */ insert into orders (date, user_id, id) values (?, ?, ?)
CascadeType.MERGE
Merge trong JPA-Hibernate giúp hợp nhất detached entity với persistent entity có cùng mã định danh. Cascade.Merge sẽ lan truyền hoạt động này sang các related entity.
@Entity @Table(name = "user") public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.MERGE) private Set<Order> orders = new HashSet<>(); // .... }
Long userId; User user = new User(); Order order = new Order(); user.getOrders().add(order); order.setUser(user); // Lưu xuống database em.persist(user); em.persist(order); em.flush(); // Cập nhật ID cho User và Order em.refresh(user); userId = user.getId(); // Clear tất cả các persisent entity em.clear(); User userSaved = em.find(User.class, userId); Order orderSaved = userSaved.getOrders().iterator().next(); // merge entity và cascade userSaved.setName("Name updated"); orderSaved.setDate(LocalDate.now()); em.merge(userSaved);
Đoạn code trên mô tả quá trình tạo và lưu trữ 2 User và Order entity xuống database, sau khi làm mới session (session#clear) thì chúng sẽ rơi vào trạng thái detached. Sau đó mình đã lấy chúng lên và cập nhật dữ liệu, lúc này trong database đã tồn tại 2 dòng dữ liệu tương ứng với User và Order. Sử dụng em.merge(user) sẽ giúp chúng ta cập nhật lại dữ liệu của User và cả Order vì chúng được ánh xạ với CasCase.MERGE.
Hibernate: /* update entities.User */ update user set name=? where id=? Hibernate: /* update entities.Order */ update orders set date=?, user_id=? where id=?
CasCade.REMOVE
Nhìn vào cái tên có lẽ chúng ta cũng đoán được phần nào, khi một entity bị xóa khởi database thì các related entity cũng bị xóa theo.
@Entity @Table(name = "user") public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) private Set<Order> orders = new HashSet<>(); // .... }
Để lấy ví dụ minh họa trước tiên chúng ta cần thêm dữ liệu vào database.
INSERT INTO `cascade`.`user` (`id`, `name`) VALUES ('1', 'name'); INSERT INTO `cascade`.`orders` (`id`, `date`, `user_id`) VALUES( 1, current_date(), 1);
User user = em.find(User.class, 1L); em.remove(user);
Sau khi thực thi đoạn code trên thì chúng ta có thể thấy rằng Hibernate đã sinh ra đến 2 câu DELETE, trước tiên nó xóa các Order liên quan đến User và sau đó tiến hành xóa luôn User.
Hibernate: /* delete entities.Order */ delete from orders where id=? Hibernate: /* delete entities.User */ delete from user where id=?
CasCade.DETACHED
EntityManager#detach dùng để xóa một entity ra khỏi persistenct context. Khi sử dụng Cascade.REMOVE, các related entity với nó cũng sẽ bị loại bỏ khởi persistence context.
@Entity @Table(name = "user") public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.DETACH) private Set<Order> orders = new HashSet<>(); // .... }
Hãy đảm bảo database của bạn có dữ liệu để có thể hoàn thành được ví dụ
INSERT INTO `cascade`.`user` (`id`, `name`) VALUES ('1', 'name'); INSERT INTO `cascade`.`orders` (`id`, `date`, `user_id`) VALUES( 1, current_date(), 1);
Sử dụng EntityManager#constains method để kiểm tra một entity có tồn tại trong Persistence context hay không
User user = em.find(User.class, 1L); Order order = user.getOrders().iterator().next(); System.out.println("Before deteach"); System.out.println("User belong to Persistence context: " + em.contains(user)); System.out.println("User belong to Persistence context: " + em.contains(order)); em.detach(user); System.out.println("After deteach"); System.out.println("User belong to Persistence context: " + em.contains(user)); System.out.println("User belong to Persistence context: " + em.contains(order));
Output
Before deteach User belong to Persistence context: true User belong to Persistence context: true After deteach User belong to Persistence context: false User belong to Persistence context: false
CascadeType.REFRESH
Trong một số trường hợp bạn đã thay đổi các thuộc tính của một persistent entity, nhưng lại muốn quay lại trạng thái ban đầu thì có thể sử dụng EntityManager#refresh, Hibernate thực hiện truy vấn và lấy dữ liệu từ database ghi đè lên các giá trị hiện có của entity. Hay một trường hợp khác chúng ta có một transaction tồn tại trong một thời gian rất lâu, trong khoảng thời gian đó có thể các luồng khác của ứng dụng đã thay đổi giá trị của một entity đang được nắm giữ, sử dụng EntityManager#refresh trong trường hợp này có thể giúp chúng ta cập nhật được giá trị mới nhất của entity.
Với CasCade.REFRESH các related entity cũng bị ghi đè giá trị từ database.
User user = new User(); user.setName("NAME A"); Order order = new Order(); order.setDate(LocalDate.of(2020, 1, 1)); order.setUser(user); user.getOrders().add(order); em.persist(user); em.persist(order); em.flush(); user.setName("NAME B"); order.setDate(LocalDate.of(2020, 2, 1)); System.out.println(user.getName()); System.out.println(order.getDate()); em.refresh(user); System.out.println(user.getName()); System.out.println(order.getDate());
Output
NAME B 2020-02-01 Hibernate: /* load entities.Order */ select order0_.id as id1_0_0_, order0_.date as date2_0_0_, order0_.user_id as user_id3_0_0_ from orders order0_ where order0_.id=? Hibernate: /* load entities.User */ select user0_.id as id1_1_1_, user0_.name as name2_1_1_, orders1_.user_id as user_id3_0_3_, orders1_.id as id1_0_3_, orders1_.id as id1_0_0_, orders1_.date as date2_0_0_, orders1_.user_id as user_id3_0_0_ from user user0_ left outer join orders orders1_ on user0_.id=orders1_.user_id where user0_.id=? NAME A 2019-12-31
Cascade.ALL
Cascade.ALL là cách sử dụng nhanh tất cả các Cascade được giới thiệu ở trên, kể cả các Cascade được Hibernate hỗ trợ riêng như REPLICATE, SAVE_UPDATE, LOCK.
Cascade.REPLICATE
Cascade.REPLICATE thường được dùng trong trường hợp chúng ta có nhiều hơn một data source và chúng ta muốn đồng bộ dữ liệu giữa chúng. Với Cascade.REPLICATE khi một entity được đồng bộ đến các data source khác nhau thì các related entity cũng được đồng bộ theo đó.
Ánh xạ REPLICATE có chút khác với các Cascade của JPA khi chúng ta phải sử dụng @Cascade annotation.
@OneToMany(mappedBy = "user") @Cascade(REPLICATE) private Set<Order> orders = new HashSet<>();
User user = new User(); user.setId(1L); user.setName("NAME A"); Order order = new Order(); order.setId(1L); order.setUser(user); user.getOrders().add(order); em.unwrap(Session.class).replicate(user, ReplicationMode.OVERWRITE);
Output
Hibernate: /* insert entities.Order */ insert into orders (date, user_id, id) values (?, ?, ?) Hibernate: /* update entities.User */ update user set name=? where id=?
Cascade.SAVE_UPDATE
Hoạt động tương tự như Casecade.PERSIST và Casecase.MERGE, khi một entity được tạo mới và lưu xuống database hay được cập nhật thì các related entity liên quan với nó cũng được lưu và cập nhật theo.
Cascade.LOCK
Cascade.LOCK được dùng để chuyển các detached entity và các related entity của nó vào lại persistence context.
User user = new User(); Order order = new Order(); order.setUser(user); user.getOrders().add(order); em.persist(user); em.persist(order); em.flush(); System.out.println("Before lock"); System.out.println("User belong to Persistence context: " + em.contains(user)); System.out.println("User belong to Persistence context: " + em.contains(order)); em.detach(user); em.detach(order); em.unwrap(Session.class) .buildLockRequest(new LockOptions(LockMode.NONE)) .lock(user); System.out.println("After lock"); System.out.println("User belong to Persistence context: " + em.contains(user)); System.out.println("User belong to Persistence context: " + em.contains(order));
Output
Before lock User belong to Persistence context: true User belong to Persistence context: true After lock User belong to Persistence context: true User belong to Persistence context: true
Tóm lược
Như vậy là chúng ta đã tìm hiểu được tất cả các Cascade mà JPA – Hibernate hỗ trợ giúp đơn hóa quá việc thao tác với các dữ liệu quan hệ. Thử nghĩ nếu không có Cascade thì chúng ta sẽ phải làm thủ công mỗi khi thao tác trên một entity và đồng bọn của chúng.
Ngoài ra về cơ bản có khá nhiều Cascade, nhưng mình thấy sử dụng nhiều là PERSIST, MERGE hoặc nhanh nhất là ALL.
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/jpa-cascade-types
https://thorben-janssen.com/hibernate-5-date-and-time/#Java_8_Support_in_Hibernate_5
A beginner’s guide to JPA and Hibernate Cascade Types