Tìm hiểu các event của một entity lifecycle trong JPA/Hibernate

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:

  1. 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.
  2. 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

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