背景
在对一个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)是非常简便的,它是一种领域驱动的思想,让代码显得更加的内聚。