在 Hibernate 中更新和插入之前更改欄位值
1. 概述
在使用 Hibernate 時,經常會出現這樣的情況:我們需要在將實體持久保存到資料庫之前更改欄位值。此類場景可能源自於使用者執行必要的欄位轉換的要求。
在本教程中,我們將採用一個簡單的範例用例,該用例在執行更新和插入之前將欄位值轉換為大寫形式。我們還將看到實現這一目標的不同方法。
2. 實體生命週期回調
首先,我們定義一個簡單的實體類別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]
3.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
實體類,而無需任何樣板程式碼。
4.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
5.Hibernate列變壓器
我們要檢查的最後一個方法是 Hibernate @ColumnTransformer
註解。此註解允許我們定義將應用於目標列的 SQL 表達式。
在下面的程式碼中,當 Hibernate 產生寫入列的 SQL 查詢時,我們透過@ColumnTransform
應用UPPER
SQL 函數來註解名稱欄位:
@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]
但是,如果我們斷言持久化實體中的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();
儘管如此,這將導致不良結果,因為我們正在清除所有其他儲存的快取實體。
六、結論
在本文中,我們探索了在將欄位值持久保存到 Hibernate 資料庫中之前更改欄位值的各種方法。這些方法包括 JPA 生命週期回呼、JPA 實體偵聽器、Hibernate 事件偵聽器和 Hibernate 列轉換器。
與往常一樣,完整的源代碼可以在 GitHub 上取得。