Spring Data之@DomainEvents註解
背景
在對一個Entity進行save操作時,往往需要觸發後續的業務流程,通常採用如下做法
public void saveUser(){
User user = ...
user = repository.save(user);
doSomething(user);
}
public void action(){
User user = ...
saveUser(user);
doSomething(user);
}
其中有一些注意事項,例如
- doSomething與saveUser在同一個事務中,需要考慮doSomething中的異常對repository.save(user)的影響
- doSomething與saveUser不在同一個事務中,那麼在doSomething中查詢user時將查詢不到,因為saveUser的事務還未提交。
這種情況則需要將doSomething上移到呼叫saveUser同級的地方呼叫這種情況則需要將doSomething上移到呼叫saveUser同級的地方呼叫
DomainEvents
近日在Spring Data的官方手冊中看到@DomainEvents的介紹。官方解釋是由Repositoty管理的Entity是源於聚合根( aggregate roots)的,在領域驅動設計系統中,可以通過聚合根發出領域事件。在Spring Data中可以通過@DomainEvents註解在聚合根的方法上,從而可以簡單快捷的發出事件。下面就來看一下,DomainEvents的具體使用效果。
首先定義一個普通的Entity
@Data
@Entity
@Table(name = "t_user")
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private Integer age;
//該方法會在userRepository.save()呼叫時被觸發呼叫
@DomainEvents
Collection<UserSaveEvent> domainEvents() {
return Arrays.asList(new UserSaveEvent(this.id));
}
}
其中UserSaveEvent的定義如下
@Data
@AllArgsConstructor
public class UserSaveEvent {
private Long id;
}
再定義一個UserService消費發出的事件
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
//接受User發出的型別為UserSaveEvent的DomainEvents事件
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void event(UserSaveEvent event){
System.out.println(userRepository.getOne(event.getId()));
}
}
其中@TransactionalEventListener註解的phase有多個選項
- BEFORE_COMMIT
- AFTER_COMMIT
- AFTER_ROLLBACK
- AFTER_COMPLETION
看名字就知道它們的作用和區別了,因為事件是repository.save發出的,這裡就涉及到了事務。通過phase的不同選項,就能選擇是在事務提交前獲取事件,還是提交後,或者混滾的時候。
執行一下單元測試
@Before
public void before(){
userRepository.saveAll(Arrays.asList(
new User(null,"劉","一", 20),
new User(null,"陳","二", 20),
new User(null,"張","三", 20),
new User(null,"李","四", 20),
new User(null,"王","五", 20),
new User(null,"趙","六", 20),
new User(null,"孫","七", 20),
new User(null,"周","八", 20)
));
}
控制檯輸出
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
User(id=1, firstName=劉, lastName=一, age=20)
User(id=2, firstName=陳, lastName=二, age=20)
User(id=3, firstName=張, lastName=三, age=20)
User(id=4, firstName=李, lastName=四, age=20)
上面是使用的phase = TransactionPhase.AFTER_COMMIT,即事務提交後響應事件,所以userRepository.getOne(event.getId())能查詢到user物件。如果改成TransactionPhase.BEFORE_COMMIT呢
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void event(UserSaveEvent event){
System.out.println(userRepository.getOne(event.getId()));
}
其實效果是一樣的也能查詢到user,難道BEFORE_COMMIT沒起作用?沒提交事務前按理是查詢不到的才對。
其實是因為session的快取,因為event方法並沒有新增@Async註解非同步,也沒有@Transactional(value = Transactional.TxType.REQUIRES_NEW)開啟新事務,所以這時與傳送事件的repository.save還在一個事務內。
如果給event方法開啟新事務
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
@Transactional(value = Transactional.TxType.REQUIRES_NEW)
public void event(UserSaveEvent event){
System.out.println(userRepository.getOne(event.getId()));
}
這樣查詢就會報錯,因為查不到了
org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find com.learn.data.entity.User with id 1; nested exception is javax.persistence.EntityNotFoundException: Unable to find com.learn.data.entity.User with id 1
再將phase改成TransactionPhase.AFTER_COMMIT試試
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(value = Transactional.TxType.REQUIRES_NEW)
public void event(UserSaveEvent event){
System.out.println(userRepository.getOne(event.getId()));
}
控制輸出
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
Hibernate: insert into t_user (id, age, first_name, last_name) values (null, ?, ?, ?)
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.first_name as first_na3_0_0_, user0_.last_name as last_nam4_0_0_ from t_user user0_ where user0_.id=?
User(id=1, firstName=劉, lastName=一, age=20)
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.first_name as first_na3_0_0_, user0_.last_name as last_nam4_0_0_ from t_user user0_ where user0_.id=?
User(id=2, firstName=陳, lastName=二, age=20)
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.first_name as first_na3_0_0_, user0_.last_name as last_nam4_0_0_ from t_user user0_ where user0_.id=?
User(id=3, firstName=張, lastName=三, age=20)
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.first_name as first_na3_0_0_, user0_.last_name as last_nam4_0_0_ from t_user user0_ where user0_.id=?
User(id=4, firstName=李, lastName=四, age=20)
現在能查詢到了,但控制檯裡面的查詢打出了select語句,與沒有新增@Transactional時是不一樣了,沒有@Transactional註解時是沒有select語句的,說明JPA查詢的是seesion快取並沒有真正執行查詢。
結束
@DomainEvents和@TransactionalEventListener的組合使用,給我們處理實體儲存後觸發事件。特別是非同步事件(給event方法加上@Async,同時開啟@EnableAsync)是非常簡便的,它是一種領域驅動的思想,讓程式碼顯得更加的內聚。