实体事件

当使用 DataManager 读写数据时,Jmix 数据访问子系统会发送特定的 Spring 应用程序事件。可以创建事件监听器对保存和加载的实体实例做一些额外的操作。

使用 EntityChangedEvent

当实体保存至数据库时,框架会发送 EntityChangedEvent 事件。可以在保存事务内或者事务结束后处理该事件,无论哪种情况,此时数据库已经保存了变更后的数据。

EntityChangedEvent 包含变更操作的类型(创建、更新或删除)、变更实体的 id,还有变更属性的相关信息以及属性的旧值。对于引用属性,旧值包含引用实体的 id。

提交之前处理变更

如需在当前数据库事务中处理 EntityChangedEvent,创建一个带 @EventListener 注解的 bean 方法。框架会在实体保存至数据库但是事务尚未提交时调用该方法。在监听器方法中,可以对数据做任何修改,这些修改会与原始的修改一并提交。如果抛出任何异常,则回退所有操作。

下面的例子中,我们创建了 Customer 的一个关联实体,用来登记属性的改动。CustomerCustomerGradeChange 的变更会在同一个事务中提交:

@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 进行新建、更新或删除,都会修改 Orderamount 属性:

@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 事件。可以在该事件中,用实体的持久化属性初始化实体的非持久化属性。

EntitySavingEventEntityLoadingEvent 监听器中,只能访问实体的本地属性。

对于实体引用,在 EntitySavingEvent 监听器中,框架不能保证引用的实体实例存在。在 EntityLoadingEvent 监听器中加载引用实体时,也无法保证能触发级联的事件。

下面的例子中,用 EntitySavingEventEntityLoadingEvent 维护一个加密属性:

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