1. 程式人生 > >Spring Data之@DomainEvents註解

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);
}

其中有一些注意事項,例如

  1. doSomething與saveUser在同一個事務中,需要考慮doSomething中的異常對repository.save(user)的影響
  2. 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)是非常簡便的,它是一種領域驅動的思想,讓程式碼顯得更加的內聚。