Spring JpaRepository 中的實體分離與附加
1. 引言
Spring JPA 簡化了持久層的查詢,無需編寫任何 Hibernate 特有的樣板程式碼。
但是,我們需要JpaRepository類別所不具備的某些行為。
這種情況涉及將實體從持久化上下文中分離並重新附加。分離實體的目的可能多種多樣,例如避免自動更新、支援多事務工作流程、提高效能或避免LazyInitializationException異常。
在本教程中,我們將學習如何在 Spring 應用程式中分離和附加實體。
2. 應用範例
假設我們需要開發一個創建新用戶的應用程式。
2.1 定義資料模型
我們將使用以下幾個欄位來定義User實體:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(unique = true)
private String email;
private boolean activated;
}
在上面的程式碼中,我們新增了一個activated欄位來儲存當前活躍用戶。
2.2. 定義JpaRepository
讓我們透過擴充JpaRepository類別來實作UserRepository介面:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {}
接下來,我們將對User實體實作一些操作。
2.3. 實現DataService服務
為了創建和啟動用戶,我們將在服務類別中實作兩個方法。
首先,我們將在UserDataService類別中實作createUser方法:
@Service
public class UserDataService {
private final UserRepository userRepository;
@Transactional
public User createUser(String name, String email) {
User user = new User();
user.setName(name);
user.setEmail(email);
user.setActivated(false);
User savedUser = userRepository.save(user);
return savedUser;
}
}
上面的程式碼創建了一個處於非活躍狀態的新用戶。
接下來,我們將在同一個UserDataService類別中實作activateUser方法:
@Transactional
public User activateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found for Id" + id));
user.setActivated(true);
userRepository.save(user);
return user;
}
在上面的程式碼中,我們僅在存在已啟動使用者時才儲存該使用者。否則,我們將拋出RuntimeException異常。
2.4. 定義 API 客戶端
我們將定義UserApiClient介面及其verify方法:
public interface UserApiClient {
boolean verify(String email);
}
為了證明這一點,我們不需要實作上面的介面。
接下來,我們將整合上面定義的所有類別來建立使用者。
2.5. 實現用戶註冊
為了協調使用者註冊流程,我們將實作一個單獨的類別來處理建立、驗證和啟動步驟。外部驗證成功後,用戶即可啟動。
我們將在UserRegistrationService類別中實作handleRegistration方法:
public User handleRegistration(String name, String email) {
User user = userDataService.createUser(name, email);
if (userApiClient.verify(email)) {
user = userDataService.activateUser(user.getId());
}
return user;
}
由於我們在事務以外的多個服務中使用User實體,因此最佳實踐是將該實體從持久化上下文中分離出來,以避免 Hibernate 的髒檢查機制導致的自動更改。此外,這樣做還能提高此類長時間運行進程的效能。
3. 分離與附加實體
我們可以使用 JPA 的支援來分離和重新附加實體。
3.1. 分離實體
要分離一個實體,我們將使用EntityManager的detach方法。
我們將把detach方法整合到現有的UserRepository介面中,因為這是一個特定的用例。
首先,我們將在DetachableRepository介面中定義一個detach方法:
public interface DetachableRepository<T> {
void detach(T t);
}
接下來,讓我們在DetachableRepositoryImpl類別中實作上述方法:
public class DetachableRepositoryImpl<T> implements DetachableRepository<T> {
@PersistenceContext
private EntityManager entityManager;
@Override
public void detach(T entity) {
if(entity != null) {
entityManager.detach(entity);
}
}
}
在上面的程式碼中, EntityManager的 detach 方法會將實體從持久化上下文中移除。本質上,被分離的實體將不再被管理和持久化。
或者,要分離所有實體,我們可以使用EntityManager的clear方法,或停用 View 中的 Open-Session,以便在交易關閉後隱含分離實體。
然後,我們將使用DetachableRepository介面擴充UserRepository介面:
@Repository
public interface UserRepository extends JpaRepository<User, Long>, DetachableRepository<User> {}
最後,我們將在使用者建立完成後,在createUser方法中加入detach方法:
userRepository.detach(savedUser);
現在,無論實體變更是在交易邊界內還是邊界外執行,都不會自動持久化。
我們可以直接在UserDataService的createUser方法中呼叫EntityManager的detach方法。但是,這樣做會降低靈活性,並引入服務層和持久層之間的耦合。
3.2. 附加實體
由於我們已經分離了該實體,因此需要重新附加它才能持久化任何更改。
要附加一個實體,有幾種方法。我們可以重新查詢並在事務中更新該實體,或明確保存同一個實體,或使用EntityManager的merge方法。我們將透過重新查詢同一個實體並在事務邊界內自動持久化所需的變更來實現它。
我們將保留帶有transactional註解activateUser方法,並移除冗餘的save方法:
@Transactional
public User activateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found for Id" + id));
user.setActivated(true);
return user;
}
需要注意的是, transactional註解會附加並儲存更新後的實體,而無需我們明確呼叫UserRepository的save方法。
我們可以將內部服務的整體流程視覺化:
接下來,我們將實現服務的整合測試。
4. 測試應用程式
我們將使用 Spring Boot 測試支援來實作UserRepository和UserRegistration測試。
4.1. 測試儲存庫
我們將測試這樣一個場景:使用者資料被儲存、分離,然後再進行更新:
@Test
void givenValidUserIsDetached_whenUserSaveIsCalled_AndUpdated_thenUserIsNotUpdated() {
User user = new User();
user.setName("test1");
user.setEmail("[email protected]");
user.setActivated(true);
userRepository.save(user);
userRepository.detach(user);
user.setName("test1_updated");
entityManager.flush();
Optional<User> savedUser = userRepository.findById(user.getId());
assertNotNull(savedUser);
assertTrue(savedUser.isPresent());
assertEquals("test1", savedUser.get().getName());
assertEquals("[email protected]", savedUser.get().getEmail());
assertTrue(savedUser.get().isActivated());
}
從上述測試中,我們確認EntityManager,因為它已從持久化上下文中分離出來。
4.2 測試服務
我們將測試UserRegistration類別的detach行為。
首先,我們將實現一個測試,其中註冊了一個有效的用戶:
@Test
void givenValidUser_whenUserIsRegistrationIsCalled_thenSaveActiveUser() {
Mockito.when(userApiClient.verify(any())).thenReturn(true);
User user = userRegistrationService.handleRegistration("test1", "[email protected]");
Optional<User> savedUser = userRepository.findById(user.getId());
assertNotNull(savedUser);
assertTrue(savedUser.isPresent());
assertEquals("test1", savedUser.get().getName());
assertEquals("[email protected]", savedUser.get().getEmail());
assertTrue(savedUser.get().isActivated());
}
我們確認所有斷言均通過,包括用戶在分離後被激活。
現在,讓我們測試一下用戶郵箱無效的另一種情況:
@Test
void givenInValidUser_whenUserIsRegistrationIsCalled_thenSaveInActiveUser() {
Mockito.when(userApiClient.verify(any())).thenReturn(false);
User user = userRegistrationService.handleRegistration("test2", "[email protected]");
Optional<User> savedUser = userRepository.findById(user.getId());
assertNotNull(savedUser);
assertTrue(savedUser.isPresent());
assertEquals("test2", savedUser.get().getName());
assertEquals("[email protected]", savedUser.get().getEmail());
assertFalse(savedUser.get().isActivated());
}
根據上述測試結果,我們確認該無效用戶處於非活躍狀態。
請注意,分離實體不應引入副作用。
5. 結論
本文介紹了在 Spring JPA 應用中分離和附加實體的原因和方法。我們使用EntityManager的detach方法和事務支援實作了相關程式碼。此外,我們也在 JPA 的儲存庫層和服務層對應用程式進行了測試。
和往常一樣,範例程式碼可以在 GitHub 上找到。