在 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
注解,可在实体持久化之前将 person
的 name
属性转换为大写:
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 集成。
如下示例演示了通过实现 PreInsertEventListener
和 PreUpdateEventListener
接口来监听预插入和预更新事件的自定义 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
(集成器)类,以便通过 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
类,其中包含我们的 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
中。因此,即使我们再次检索,它也会返回相同的实体。要获得转换后的更新了的实体,我们需要先调用 EntityManager
的 clear()
方法来清除缓存实体:
entityManager.clear();
然而,这将导致一个不良后果,因为我们清除了所有其他已存储的缓存实体。
6、总结
本文介绍了在 Hibernate 中将字段值持久化到数据库之前更改字段值的各种方法,这些方法包括 JPA 生命周期回调、JPA 实体监听器、Hibernate 事件监听器和 Hibernate 列转换器。
Ref:https://www.baeldung.com/java-hibernate-change-field-value-before-update-insert