实体事件
当使用 DataManager
读写数据时,Jmix 数据访问子系统会发送特定的 Spring 应用程序事件。可以创建事件监听器对保存和加载的实体实例做一些额外的操作。
使用 EntityChangedEvent
当实体保存至数据库时,框架会发送 EntityChangedEvent
事件。可以在保存事务内或者事务结束后处理该事件,无论哪种情况,此时数据库已经保存了变更后的数据。
EntityChangedEvent
包含变更操作的类型(创建、更新或删除)、变更实体的 id,还有变更属性的相关信息以及属性的旧值。对于引用属性,旧值包含引用实体的 id。
提交之前处理变更
如需在当前数据库事务中处理 EntityChangedEvent
,创建一个带 @EventListener
注解的 bean 方法。框架会在实体保存至数据库但是事务尚未提交时调用该方法。在监听器方法中,可以对数据做任何修改,这些修改会与原始的修改一并提交。如果抛出任何异常,则回退所有操作。
下面的例子中,我们创建了 Customer
的一个关联实体,用来登记属性的改动。Customer
和 CustomerGradeChange
的变更会在同一个事务中提交:
@Component
public class CustomerEventListener {
@Autowired
private DataManager dataManager;
@EventListener
void onCustomerChangedBeforeCommit(EntityChangedEvent<Customer> event) {
if (event.getType() != EntityChangedEvent.Type.DELETED (1)
&& event.getChanges().isChanged("grade")) { (2)
registerGradeChange(
event.getEntityId(), (3)
event.getChanges().getOldValue("grade") (4)
);
}
}
private void registerGradeChange(Id<Customer> customerId, CustomerGrade oldGrade) {
Customer customer = dataManager.load(customerId).one(); (5)
CustomerGradeChange gradeChange = dataManager.create(CustomerGradeChange.class);
gradeChange.setCustomer(customer);
gradeChange.setOldGrade(oldGrade);
gradeChange.setNewGrade(customer.getGrade());
dataManager.save(gradeChange);
}
1 | 确定变更类型。 |
2 | 确认属性修改。 |
3 | 获取变更实体的 id。 |
4 | 获取变更属性的旧值。 |
5 | 加载变更实体的新状态。 |
我们看看另一个例子。如果对 Order
实体的 OrderLine
进行新建、更新或删除,都会修改 Order
的 amount
属性:
@Component
public class OrderLineEventListener {
@Autowired
private DataManager dataManager;
@EventListener
void onOrderLineChangedBeforeCommit(EntityChangedEvent<OrderLine> event) {
Order order;
if (event.getType() == EntityChangedEvent.Type.DELETED) { (1)
Id<Order> orderId = event.getChanges().getOldReferenceId("order"); (2)
order = dataManager.load(orderId).one();
} else {
OrderLine orderLine = dataManager.load(event.getEntityId()).one();
order = orderLine.getOrder();
}
BigDecimal amount = order.getLines().stream()
.map(line -> line.getProduct().getPrice().multiply(
BigDecimal.valueOf(line.getQuantity()))
)
.reduce(BigDecimal.ZERO, BigDecimal::add);
order.setAmount(amount);
dataManager.save(order);
}
}
1 | 当 OrderLine 实体被删除时,我们无法加载该实例,所以只能用旧值加载关联的 Order 。 |
2 | 用 getOldReference() 和 getOldCollection() 而非 getOldValue() 加载对一和对多的引用属性。 |
提交之后处理变更
如需在变更提交至数据库完成之后处理 EntityChangedEvent
,创建一个带 @TransactionalEventListener
注解的 bean 方法。
注意,此时在“提交之后”事件处理器中的异常不会返回给调用端,也不会记录。因此,推荐在 try-catch 中调用代码。
如需在“提交之后”事件处理器中加载或保存任何数据,都需要新起一个事务。
下面的例子中展示了异常处理和使用 DataManager
在单独的事务中加载实体(参考 joinTransaction(false)
方法):
@Component
public class CustomerEventListener {
private static final Logger log = LoggerFactory.getLogger(CustomerEventListener.class);
@Autowired
private DataManager dataManager;
@TransactionalEventListener
void onCustomerChangedAfterCommit(EntityChangedEvent<Customer> event) {
try {
if (event.getType() != EntityChangedEvent.Type.DELETED
&& event.getChanges().isChanged("grade")) {
Customer customer = dataManager.load(event.getEntityId())
.joinTransaction(false)
.one();
emailCustomerTheirNewGrade(customer.getEmail(), customer.getGrade());
}
} catch (Exception e) {
log.error("Error handling Customer changes after commit", e);
}
}
如需在“提交之后”事件处理器中保存实体,使用 SaveContext
及其 setJoinTransaction(false)
方法,示例:
dataManager.save(new SaveContext()
.saving(entity)
.setJoinTransaction(false)
);
如果需要多次调用 DataManager
或其他需要使用事务的 service,那么可以为整个方法启动一个新的事务,示例:
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW) (1)
void onCustomerChangedAfterCommit2(EntityChangedEvent<Customer> event) {
// ...
}
1 | 使用 Propagation.REQUIRES_NEW 启动新事务。 |
使用 EntitySavingEvent 和 EntityLoadingEvent
框架在将要保存至数据库时,会发送 EntitySavingEvent
事件。与包含实体 id 的 EntityChangedEvent 不同,EntitySavingEvent
包含实体本身。可以在保存至数据库之前修改实体的信息。
如果是在数据库表新增实例时发出的事件,则事件的 isNewEntity()
方法返回 true。
可以用 EntitySavingEvent
监听器在实体保存至数据库之前初始化实体属性。示例:
@Component
public class OrderEventListener {
@EventListener
void onOrderSaving(EntitySavingEvent<Order> event) {
if (event.isNewEntity()) {
Order order = event.getEntity();
order.setNumber(generateOrderNumber());
}
}
当实体实例从数据库加载时,框架会发送 EntityLoadingEvent
事件。可以在该事件中,用实体的持久化属性初始化实体的非持久化属性。
在 对于实体引用,在 |
下面的例子中,用 EntitySavingEvent
和 EntityLoadingEvent
维护一个加密属性:
@JmixEntity
@Table(name = "CUSTOMER")
@Entity(name = "sample_Customer")
public class Customer {
@Column(name = "ENCRYPTED_DATA")
@Lob
private String encryptedData;
@Transient
@JmixProperty
private String sensitiveData;
当实体保存实体时,敏感内容会被加密之后再存入数据库。而加载时,内容会先被解密然后设置到非持久化属性中以便用户访问:
@Component
public class CustomerEventListener {
@Autowired
private EncryptionService encryptionService;
@EventListener
void onCustomerSaving(EntitySavingEvent<Customer> event) {
Customer customer = event.getEntity();
String encrypted = encryptionService.encrypt(customer.getSensitiveData());
customer.setEncryptedData(encrypted);
}
@EventListener
void onCustomerLoading(EntityLoadingEvent<Customer> event) {
Customer customer = event.getEntity();
String sensitive = encryptionService.decrypt(customer.getEncryptedData());
customer.setSensitiveData(sensitive);
}