在 Hibernate 中更新和插入前更改字段值

1、概览

在使用 Hibernate 时,经常会遇到这样的情况:在将实体持久化到数据库之前,需要更改字段的值。这种情况可能是因为需要执行必要的字段转换。

本文将通过一个示例:即在执行更新和插入操作前将字段值转换为大写字母,来了解实现这一目的的不同方法。

2、实体生命周期回调

首先,定义一个简单的实体类 Student

@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column
    private String name;

    // Getter / Setter 方法省略
}

第一种方法是 JPA 实体生命周期回调。JPA 提供了一组注解,允许我们在不同的 JPA 生命周期事件中执行一个方法,例如:

  • @PrePresist:在插入事件之前执行。
  • @PreUpdate:在更新事件之前执行。

我们在 Student 实体类中添加 changeNameToUpperCase() 方法。该方法将 name 字段改为大写。该方法通过 @PrePersist@PreUpdate 进行注解,以便 JPA 在持久化和更新之前调用该方法:

@Entity
@Table(name = "student")
public class Student {

    @PrePersist
    @PreUpdate
    private void changeNameToUpperCase() {
        name = StringUtils.upperCase(name);
    }

    // 其他内容和上述类中的定义一样
}

现在,运行下面的代码来持久化一个新的 Student 实体:

Student student = new Student();
student.setName("David Morgan");
entityManager.persist(student);

我们可以从控制台日志中看到,name 参数在加入 SQL 查询之前已被转换为大写字母:

[main] DEBUG org.hibernate.SQL - insert into student (name,id) values (?,default)
Hibernate: insert into student (name,id) values (?,default)
[main] TRACE org.hibernate.orm.jdbc.bind - binding parameter (1:VARCHAR) <- [DAVID MORGAN]

3、JPA 实体监听器

我们在实体类内部定义了回调方法来处理 JPA 生命周期事件。如果有多个实体类需要实现相同逻辑,这种做法往往会显得重复。例如,我们需要实现所有实体类共同使用的审计和日志记录功能,但在每个实体类内部定义相同的回调方法被认为是代码重复。

JPA 提供了使用这些回调方法定义实体监听器(Entity Listener)的选项。事件监听器使 JPA 生命周期回调方法与实体类分离,从而减少代码重复。

现在让我们看看同样的大写转换场景,并将逻辑应用于不同的实体类,但这次我们使用事件监听器来实现它。

先定义一个 Person 接口,作为所有实体类的父类,以便在多个实体类上应用相同的逻辑:

public interface Person {
    String getName();
    void setName(String name);
}

该接口允许实现一个通用的实体监听器类,该类适用于每个 Person 的实现。在事件监听器中,方法 changeNameToUpperCase() 具有 @PrePersist@PreUpdate 注解,可在实体持久化之前将 personname 属性转换为大写:

public class PersonEventListener<T extends Person> {
    @PrePersist
    @PreUpdate
    private void changeNameToUpperCase(T person) {
        person.setName(StringUtils.upperCase(person.getName()));
    }
}

现在,为了完成配置,我们需要配置 Hibernate,以在应用中注册我们的 Provider。我们在示例中使用的是 Spring Boot

application.yaml 中添加 integrator_provider 属性:

@Entity
@Table(name = "student")
@EntityListeners(PersonEventListener.class)
public class Student implements Person {
    // 定义和基类中相同
}

它所做的事情与上述示例完全相同,但可重用性更强:它将转换大写字母的逻辑从实体类本身移出,放到了实体监听器类中。因此,我们可以将这一逻辑应用于任何实现 Person 的实体类,而无需任何模板代码。

4、Hibernate 实体监听器

Hibernate 通过其专用事件系统提供了另一种处理实体生命周期事件的机制。它允许我们定义事件监听器,并将其与 Hibernate 集成。

如下示例演示了通过实现 PreInsertEventListenerPreUpdateEventListener 接口来监听预插入和预更新事件的自定义 Hibernate 事件监听器:

public class HibernateEventListener implements PreInsertEventListener, PreUpdateEventListener {

    @Override
    public boolean onPreInsert(PreInsertEvent event) {
        upperCaseStudentName(event.getEntity());
        return false;
    }

    @Override
    public boolean onPreUpdate(PreUpdateEvent event) {
        upperCaseStudentName(event.getEntity());
        return false;
    }

    private void upperCaseStudentName(Object entity) {
        if (entity instanceof Student) {
            Student student = (Student) entity;
            student.setName(StringUtils.upperCase(student.getName()));
        }
    }

}

每个接口都要求我们实现一个事件处理方法。在这两个方法中,我们都将调用 upperCaseStudentName() 方法。这个自定义事件监听器将尝试拦截 name 字段,并在 Hibernate 插入或更新之前将其设置为大写。

定义完事件监听器类后,让我们定义一个 Integrator集成器)类,以便通过 HibernateEventListenerRegistry 注册我们的自定义事件监听器:

public class HibernateEventListenerIntegrator implements Integrator {

    @Override
    public void integrate(Metadata metadata, BootstrapContext bootstrapContext, 
      SessionFactoryImplementor sessionFactoryImplementor) {
        ServiceRegistryImplementor serviceRegistry = sessionFactoryImplementor.getServiceRegistry();
        EventListenerRegistry eventListenerRegistry = serviceRegistry.getService(EventListenerRegistry.class);
        HibernateEventListener listener = new HibernateEventListener();
        eventListenerRegistry.appendListeners(EventType.PRE_INSERT, listener);
        eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, listener);
    }

    @Override
    public void disintegrate(SessionFactoryImplementor sessionFactory, 
      SessionFactoryServiceRegistry serviceRegistry) {
    }
}

此外,我们还创建了一个自定义 IntegratorProvider 类,其中包含我们的 integrator。该 Provider 将在 Hibernate 配置中被引用,以确保我们的自定义 integrator 在应用启动时被注册:

public class HibernateEventListenerIntegratorProvider implements IntegratorProvider {
    @Override
    public List<Integrator> getIntegrators() {
        return Collections.singletonList(new HibernateEventListenerIntegrator());
    }
}

要完成设置,我们必须配置 Hibernate,以便在应用中注册我们的 Provider。我们在示例中采用了 Spring Boot

application.yaml 中添加 integrator_provider 属性:

spring:
  jpa:
    properties:
      hibernate:
        integrator_provider: com.baeldung.changevalue.entity.event.StudentIntegratorProvider

5、Hibernate Column Transformer(列转换器)

最后一种方法是 Hibernate 的 @ColumnTransformer 注解。该注解允许我们定义一个适用于目标列的 SQL 表达式。

在下面的代码中,我们通过 @ColumnTransform 注解 name 字段,当 Hibernate 生成写入列的 SQL 查询时,应用 UPPER SQL 函数:

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column
    @ColumnTransformer(write = "UPPER(?)")
    private String name;

    // Getter / Setter 方法省略
}

这种方法看起来简单明了,但有一个重大缺陷。转换只在数据库级别进行。如果我们在表中插入一行,就会在控制台日志中看到以下包含 UPPER 函数的 SQL:

[main] DEBUG org.hibernate.SQL - insert into student (name,id) values (UPPER(?),default)
Hibernate: insert into student (name,id) values (UPPER(?),default)
[main] TRACE org.hibernate.orm.jdbc.bind - binding parameter (1:VARCHAR) <- [David Morgan]

但是,如果我们断言持久化实体中的 name,就会发现实体中的 name 不是大写的:

@Test
void whenPersistStudentWithColumnTranformer_thenNameIsNotInUpperCase() {
    Student student = new Student();
    student.setName("David Morgan");

    entityManager.persist(student);
    assertThat(student.getName()).isNotEqualTo("DAVID MORGAN");
}

这是因为实体已经缓存在 EntityManager 中。因此,即使我们再次检索,它也会返回相同的实体。要获得转换后的更新了的实体,我们需要先调用 EntityManagerclear() 方法来清除缓存实体:

entityManager.clear();

然而,这将导致一个不良后果,因为我们清除了所有其他已存储的缓存实体。

6、总结

本文介绍了在 Hibernate 中将字段值持久化到数据库之前更改字段值的各种方法,这些方法包括 JPA 生命周期回调、JPA 实体监听器、Hibernate 事件监听器和 Hibernate 列转换器。


Ref:https://www.baeldung.com/java-hibernate-change-field-value-before-update-insert