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

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

使用 Hibernate 时,经常会出现需要在将实体持久化到数据库之前更改字段值的情况。此类情况可能是由于用户要求执行必要的字段转换而产生的。

在本教程中,我们以一个简单的示例用例为例,它在执行更新和插入之前将字段值转换为大写。我们还将看到实现此目的的不同方法。

实体生命周期回调
首先我们定义一个简单的实体类Student来举例:

@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column
    private String name;
   
// getters and setters
}

我们要回顾的第一种方法是 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);
    }
   
// The same definitions in our base class
}

现在,让我们运行以下代码来持久化一个新的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]


JPA 实体监听器
我们在实体类中定义了回调方法来处理 JPA 生命周期事件。如果我们有多个实体类需要实现相同的逻辑,那么这往往会重复。例如,我们需要实现所有实体类都通用的审计和日志记录功能,但在每个实体类中定义相同的回调方法会被视为代码重复。

JPA 提供了一个选项,可以使用这些回调方法定义实体侦听器。事件侦听器将 JPA 生命周期回调方法与实体类分离,以减少代码重复。

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

让我们首先定义一个Person接口作为我们解决方案的扩展,以便将相同的逻辑应用于多个实体类:

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

此接口允许实现一个通用实体侦听器类,该类将适用于每个Person实现。在事件侦听器中,方法changeNameToUpperCase()具有@PrePersist和@PreUpdate注释,可在实体持久化之前将人员姓名转换为大写:

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

现在,为了完成配置,我们需要配置 Hibernate 以在应用程序中注册我们的提供程序。我们在示例中使用 Spring Boot。让我们将integrator_provider属性添加到application.yaml中:

@Entity
@Table(name = "student")
@EntityListeners(PersonEventListener.class)
public class Student implements Person {
   
// The same definitions in our base class 
}

它的功能与上述示例完全相同,但采用更易于重用的方式:它将转换大写的逻辑从实体类本身移出,并将其放入其实体侦听器类中。因此,我们可以将此逻辑应用于任何实现Person的实体类,而无需任何样板代码。

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

我们的下一个示例演示了一个自定义的 Hibernate 事件监听器,它通过实现PreInsertEventListener和PreUpdateEventListener接口来监听预插入和预更新事件 :

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()方法。此自定义事件侦听器将尝试拦截名称字段,并在 Hibernate 插入或更新之前将其变为大写。

定义完我们的事件监听器类之后,我们来定义一个Integrator类来通过Hibernate的EventListenerRegistry来注册我们自定义的事件监听器:

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类。此提供程序将在我们的 Hibernate 配置中引用,以确保我们的自定义集成器在应用程序启动期间注册:

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

要完成设置,我们必须配置 Hibernate 以在应用程序中注册我们的提供程序。我们在示例中采用 Spring Boot。让我们将属性integrator_provider添加到application.yaml中:

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

Hibernate 列转换器
我们将研究的最后一种方法 是 Hibernate  @ColumnTransformer注释。此注释允许我们定义 将应用于目标列的 SQL 表达式。

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

@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column
    @ColumnTransformer(write =
"UPPER(?)")
    private String name;
   
// getters and setters
}

这种方法看起来很简单,但有一个重大缺陷。转换仅发生在数据库级别。如果我们在表中插入一行,我们将在控制台日志中看到以下带有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]

但是,如果我们从持久化实体断言名称的大小写,我们可以看到实体中的名称不是大写的:

@Test
void whenPersistStudentWithColumnTranformer_thenNameIsNotInUpperCase() {
    Student student = new Student();
    student.setName("David Morgan");
    entityManager.persist(student);
    assertThat(student.getName()).isNotEqualTo(
"DAVID MORGAN");
}

这是因为该实体已缓存在 EntityManager 中。因此,即使我们再次检索它,它也会将相同的实体返回给我们。要获取具有转换结果的更新实体,我们需要首先通过调用EntityManager上的clear()方法来清除缓存的实体:

entityManager.clear();

尽管如此,这将导致不良结果,因为我们正在清除所有其他存储的缓存实体。