«1. Введение

При работе с JPA есть несколько событий, о которых мы можем получать уведомления в течение жизненного цикла объекта. В этом руководстве мы обсудим события жизненного цикла объекта JPA и то, как мы можем использовать аннотации для обработки обратных вызовов и выполнения кода при возникновении этих событий.

Мы начнем с аннотирования методов самой сущности, а затем перейдем к использованию слушателя сущности.

2. События жизненного цикла объекта JPA

JPA определяет семь необязательных событий жизненного цикла, которые вызываются:

    перед вызовом persist для нового объекта — @PrePersist после вызова persist для нового объекта — @ PostPersist перед удалением объекта – @PreRemove после удаления объекта – @PostRemove перед операцией обновления – @PreUpdate после обновления объекта – @PostUpdate после загрузки объекта – @PostLoad

Существует два подхода к использованию аннотаций событий жизненного цикла: аннотирование методов в сущности и создание EntityListener с аннотированными методами обратного вызова. Мы также можем использовать оба одновременно. Независимо от того, где они находятся, методы обратного вызова должны иметь возвращаемый тип void.

Итак, если мы создадим новую сущность и вызовем метод сохранения нашего репозитория, будет вызван наш метод, аннотированный @PrePersist, затем запись будет вставлена ​​в базу данных, и, наконец, будет вызван наш метод @PostPersist. Если мы используем @GeneratedValue для автоматического создания наших первичных ключей, мы можем ожидать, что этот ключ будет доступен в методе @PostPersist.

Для операций @PostPersist, @PostRemove и @PostUpdate в документации упоминается, что эти события могут произойти сразу после выполнения операции, после сброса или в конце транзакции.

Мы должны отметить, что обратный вызов @PreUpdate вызывается только в том случае, если данные действительно изменены, то есть, если есть фактический оператор обновления SQL для запуска. Обратный вызов @PostUpdate вызывается независимо от того, действительно ли что-то изменилось.

Если какой-либо из наших обратных вызовов для сохранения или удаления сущности выдает исключение, транзакция будет отменена.

3. Аннотирование объекта

Давайте начнем с использования аннотаций обратного вызова непосредственно в нашем объекте. В нашем примере мы собираемся оставить журнальный след при изменении записей пользователя, поэтому мы собираемся добавить простые операторы ведения журнала в наши методы обратного вызова.

Кроме того, мы хотим убедиться, что собираем полное имя пользователя после его загрузки из базы данных. Мы сделаем это, аннотировав метод с помощью @PostLoad.

Мы начнем с определения нашего объекта User:

@Entity
public class User {
    private static Log log = LogFactory.getLog(User.class);

    @Id
    @GeneratedValue
    private int id;
    
    private String userName;
    private String firstName;
    private String lastName;
    @Transient
    private String fullName;

    // Standard getters/setters
}

Далее нам нужно создать интерфейс UserRepository:

public interface UserRepository extends JpaRepository<User, Integer> {
    public User findByUserName(String userName);
}

Теперь давайте вернемся к нашему классу User и добавим наши методы обратного вызова: ~ ~~

@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;
}

Когда мы запустим наши тесты, мы увидим серию операторов регистрации, исходящих от наших аннотированных методов. Кроме того, мы можем с уверенностью ожидать, что полное имя нашего пользователя будет заполнено при загрузке пользователя из базы данных.

4. Аннотирование EntityListener

Теперь мы собираемся расширить наш пример и использовать отдельный EntityListener для обработки наших обратных вызовов обновления. Мы могли бы предпочесть этот подход размещению методов в нашей сущности, если у нас есть какая-то операция, которую мы хотим применить ко всем нашим сущностям.

Давайте создадим наш AuditTrailListener для регистрации всей активности в таблице User:

public class AuditTrailListener {
    private static Log log = LogFactory.getLog(AuditTrailListener.class);
    
    @PrePersist
    @PreUpdate
    @PreRemove
    private void beforeAnyUpdate(User user) {
        if (user.getId() == 0) {
            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());
    }
}

Как видно из примера, мы можем применять несколько аннотаций к методу.

Теперь нам нужно вернуться к нашей сущности User и добавить аннотацию @EntityListener к классу:

@EntityListeners(AuditTrailListener.class)
@Entity
public class User {
    //...
}

И, когда мы запустим наши тесты, мы получим два набора сообщений журнала для каждого обновления. действие и сообщение журнала после загрузки пользователя из базы данных.

5. Заключение

В этой статье мы узнали, что такое обратные вызовы жизненного цикла объекта JPA и когда они вызываются. Мы посмотрели аннотации и поговорили о правилах их использования. Мы также экспериментировали с их использованием как в классе сущностей, так и в классе EntityListener.

Код примера доступен на GitHub.