Mục lục
Khi làm việc với JPA, chúng ta sẽ được thông báo các sự kiện trong vòng đời của một entity. Dựa vào những sự kiện này, chúng ta có thể cài đặt các mã code để thực thi một số tác vụ nhất định. Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu cách sử dụng các annotation để bắt các sự kiện khi chúng xảy ra.
Các sự kiện trong vòng đời của một entity
JPA cung cấp 7 sự kiện trong vòng đời của một entity:
- Trước khi lưu một entity mới – @PrePersist
- Sau khi lưu một entity mới – @PostPersist
- Trước khi một entity bị xoá – @PreRemove
- Sau khi một entity đã bị xoá – @PostRemove
- Trước khi cập nhật một entity – @PreUpdate
- Sau khi entity đã được cập nhật – @PostUpdate
- Sau khi một entity đã được tải – @PostLoad
Chúng ta có 2 cách để bắt các sự kiện trong vòng đời của một entity:
- Sử dụng các annotation trên (@PrePersist, @PostPersist, @PreRemove v.v) để chú thích các method sẽ được gọi khi các sự kiện tương ứng xảy đến.
- Khởi tạo một class riêng định nghĩa các method tương ứng với các sự kiện, tuy nhiên chúng ta vẫn phải chú thích các annotation. Cuối cùng chúng ta chỉ định class này với @EntityListeners để JPA có thể hiểu rằng đây là một class dùng để xử lý sự kiện cho một entity cụ thể.
Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu cả 2 cách này, tuy nhiên một điểm quan trọng các bạn cần phải biết đó là các method dùng để xử lý sự kiện phải có kiểu trả về là void nhé.
Maven dependency
Trước khi bắt đầu đi vào thực nghiệm chúng ta cần sử dụng một số dependency trong project Spring Boot.
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> <scope>provided</scope> </dependency> </dependencies>
Trong đó H2 được dùng làm database với mục đích gọn nhẹ và giảm thiểu một số cấu hình phức tạp.
Annotation entity
Chúng ta sẽ bắt đầu bằng cách sử dụng các annotation trực tiếp trên entity.
package com.deft.entity; import lombok.Getter; import lombok.Setter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.persistence.*; @Entity @Getter @Setter public class User { private static Log log = LogFactory.getLog(User.class); @Id @GeneratedValue private Integer id; private String userName; private String firstName; private String lastName; @Transient private String fullName; @PrePersist public void logNewUserAttempt() { log.info("Attempting to add new user with username: " + userName); } @PostPersist public void logNewUserAdded() { log.info("Added user '" + userName + "' with ID: " + id); } @PreRemove public void logUserRemovalAttempt() { log.info("Attempting to delete user: " + userName); } @PostRemove public void logUserRemoval() { log.info("Deleted user: " + userName); } @PreUpdate public void logUserUpdateAttempt() { log.info("Attempting to update user: " + userName); } @PostUpdate public void logUserUpdate() { log.info("Updated user: " + userName); } @PostLoad public void logUserLoad() { fullName = firstName + " " + lastName; } }
Tiếp theo tạo một UserRepository để thao tác với H2 database.
package com.deft.repository; import com.deft.entity.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Integer> { }
Trong Spring Boot, khi chúng ta tạo một entity mới và gọi method save(), thì method được chú thích với @PrePersist sẽ được gọi, sau đó khi entity này được lưu xuống database, method được chú thích với @PostPersist sẽ được gọi.
package com.deft; import com.deft.entity.User; import com.deft.repository.UserRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class JpaEntityLifecycleEventApplicationTests { @Autowired private UserRepository userRepository; @Test public void newEntityEvent() { User user = new User(); user.setUserName("Deft"); user.setFirstName("Hai"); user.setLastName("Nguyen"); userRepository.save(user); } }
Sau khi chạy unit-test trên chúng ta sẽ có kết quả như sau:
2021-01-19 00:49:57.930 INFO 7189 --- [ main] .JpaEntityLifecycleEventApplicationTests : Started JpaEntityLifecycleEventApplicationTests in 4.906 seconds (JVM running for 6.896) 2021-01-19 00:49:58.071 INFO 7189 --- [ main] com.deft.entity.User : Attempting to add new user with username: Deft 2021-01-19 00:49:58.122 INFO 7189 --- [ main] com.deft.entity.User : Added user 'Deft' with ID: 1
Đối với các sự kiện @PostPersist, @PostRemove và @PostUpdate, chúng sẽ được gọi ngay sau khi các hành động tương ứng xảy ra, hoặc sau khi sử dụng flush() method hoặc khi một transaction kết thúc.
Một lưu ý nữa là @PreUpdate method chỉ được gọi khi dữ liệu thực sự bị thay đổi.
@Test public void updateEntity() { User user = new User(); user.setUserName("Deft"); user.setFirstName("Hai"); user.setLastName("Nguyen"); user = userRepository.save(user); user.setUserName("Deft change"); userRepository.save(user); }
Output
2021-01-19 00:58:05.714 INFO 7265 --- [ main] com.deft.entity.User : Attempting to add new user with username: Deft 2021-01-19 00:58:05.761 INFO 7265 --- [ main] com.deft.entity.User : Added user 'Deft' with ID: 1 2021-01-19 00:58:05.791 INFO 7265 --- [ main] com.deft.entity.User : Attempting to update user: Deft change 2021-01-19 00:58:05.795 INFO 7265 --- [ main] com.deft.entity.User : Updated user: Deft change
Annotation EntityListener
Bây giờ chúng ta sẽ mở rộng ví dụ của mình và sử dụng EntityListener riêng để xử lý các sự kiện. Mình khuyên nên sử dụng cách này thay vì định nghĩa các event method trực tiếp bên trong entity, điều này giúp cho mã nguồn của entity ngắn gọn và dễ đọc hơn, ngoài ra chúng ta có thể định nghĩa một EntityListener class dùng chung cho nhiều entity.
Giờ giả sử chúng ta tạo một AuditTrailListener class dùng để bắt sự kiện cho bảng User.
package com.deft.entity; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.persistence.*; public class AuditTrailListener { private static Log log = LogFactory.getLog(AuditTrailListener.class); @PrePersist @PreUpdate @PreRemove private void beforeAnyUpdate(User user) { if (user.getId() == null) { log.info("[USER AUDIT] About to add a user"); } else { log.info("[USER AUDIT] About to update/delete user: " + user.getId()); } } @PostPersist @PostUpdate @PostRemove private void afterAnyUpdate(User user) { log.info("[USER AUDIT] add/update/delete complete for user: " + user.getId()); } @PostLoad private void afterLoad(User user) { log.info("[USER AUDIT] user loaded from database: " + user.getId()); } }
Các bạn lưu ý chúng ta có thể dùng nhiều annotation trên cùng một method, đồng nghĩa với việc method này sẽ được gọi cho nhiều sự kiện khác nhau trong vòng đời của một entity.
Sau đó chúng ta chỉ cần sử dụng @EntityListeners annotation để chỉ định AuditTrailListener class được sử dụng để xử lý các sự kiện cho User entity.
package com.deft.entity; import lombok.Getter; import lombok.Setter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.persistence.*; @Entity @Getter @Setter @EntityListeners(AuditTrailListener.class) public class User { private static Log log = LogFactory.getLog(User.class); @Id @GeneratedValue private Integer id; private String userName; private String firstName; private String lastName; @Transient private String fullName; }
Sau khi chạy lại unit-test sau
@Test public void updateEntity() { User user = new User(); user.setUserName("Deft"); user.setFirstName("Hai"); user.setLastName("Nguyen"); user = userRepository.save(user); user.setUserName("Deft change"); userRepository.save(user); }
Output
2021-01-19 01:08:44.759 INFO 7360 --- [ main] com.deft.entity.AuditTrailListener : [USER AUDIT] About to add a user 2021-01-19 01:08:44.800 INFO 7360 --- [ main] com.deft.entity.AuditTrailListener : [USER AUDIT] add/update/delete complete for user: 1 2021-01-19 01:08:44.840 INFO 7360 --- [ main] com.deft.entity.AuditTrailListener : [USER AUDIT] user loaded from database: 1 2021-01-19 01:08:44.841 INFO 7360 --- [ main] com.deft.entity.AuditTrailListener : [USER AUDIT] About to update/delete user: 1 2021-01-19 01:08:44.844 INFO 7360 --- [ main] com.deft.entity.AuditTrailListener : [USER AUDIT] add/update/delete complete for user: 1
Kết bài
Qua bài viết này chúng ta đã biết cách bắt và xử lý các sự kiện xảy ra trong vòng đời của một JPA entity. Trong đó cách tạo ra một class riêng để xử lý được khuyến khích hơn vì nó làm giảm độ phức tạp cho entity class, ngoài ra có thể sử dụng lại cho nhiều entity khác nhau.
Cuối cùng mã nguồn được mình công khai trên gitlab để mọi người có thể tiện tham khảo: jpa-entity-lifecycle
Nguồn tham khảo
https://www.baeldung.com/jpa-entity-lifecycle-events