3.4.7. EntityChangedEvent

Руководство Decouple Business Logic with Application Events демонстрирует использование EntityChangedEvent.

EntityChangedEvent - это ApplicationEvent, который посылается фреймворком на среднем слое, когда некоторый экземпляр сущности сохраняется в базу данных. Данное событие может быть обработано как внутри текущей транзакции (используя @EventListener), так и после ее завершения (при использовании @TransactionalEventListener).

Событие посылается только если на сущности есть аннотация @PublishEntityChangedEvents. Не забудьте добавить эту аннотацию классам сущностей, для которых вы хотите обрабатывать EntityChangedEvent.

Объект EntityChangedEvent содержит не сам измененный экземпляр сущности, а только его id. Кроме того, метод getOldValue(attributeName) возвращает идентификаторы ссылок вместо самих объектов. Поэтому при необходимости, разработчик должен загрузить требуемые сущности с указанием требуемого представления и других параметров.

Ниже приведен пример обработки EntityChangedEvent для сущности Customer в текущей транзакции и после ее завершения:

package com.company.demo.core;

import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.AttributeChanges;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.UUID;

@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {

    @EventListener (1)
    public void beforeCommit(EntityChangedEvent<Customer, UUID> event) {
        Id<Customer, UUID> entityId = event.getEntityId(); (2)
        EntityChangedEvent.Type changeType = event.getType(); (3)

        AttributeChanges changes = event.getChanges();
        if (changes.isChanged("name")) { (4)
            String oldName = changes.getOldValue("name"); (5)
            // ...
        }
    }

    @TransactionalEventListener (6)
    public void afterCommit(EntityChangedEvent<Customer, UUID> event) {
        (7)
    }
}
1 - данный обработчик вызывается внутри текущей транзакции.
2 - id измененной сущности.
3 - тип изменения: CREATED, UPDATED or DELETED.
4 - можно проверить, изменился ли определенный атрибут.
5 - можно получить старое значение измененного атрибута.
6 - данный обработчик вызывается после коммита транзакции.
7 - после коммита событие содержит те же значения что и внутри транзакции.

Если обработчик вызывается внутри транзакции, ее можно откатить путем выбрасывания исключения. При этом в БД никакие изменения не сохранятся. Если вы не хотите, чтобы пользователь получил какое-либо оповещение, используйте SilentException.

Если "after commit" обработчик выбрасывает исключение, оно будет залоггировано, но не передано клиенту (т.е. пользователь не получит сообщения об ошибке в UI).

При обработке EntityChangedEvent внутри транзакции (@EventListener или @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT), обязательно используйте TransactionalDataManager для загрузки текущего состояния измененной сущности из базы данных. Если использовать DataManager, то будет создана новая транзакция, что может повлечь дедлок в базе данных при попытке чтения незакоммиченных данных.

В обработчике, вызываемом после коммита транзакции (TransactionPhase.AFTER_COMMIT), используйте DataManager, или создайте новую транзакцию явно перед использованием TransactionalDataManager.

Ниже приведен пример использования EntityChangedEvent для изменения связанных сущностей.

Предположим, имеются сущности Order, OrderLine и Product, как в приложении Sales, но сущность Product дополнительно имеет булевский атрибут special, а у сущности Order есть атрибут numberOfSpecialProducts, который должен быть пересчитан при создании и удалении экземпляров OrderLine в составе Order.

Создадим следующий класс с методом, аннотированным @EventListener, который будет вызываться при изменении сущностей OrderLine перед коммитом транзакции:

package com.company.sales.listener;

import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.core.TransactionalDataManager;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import javax.inject.Inject;
import java.util.UUID;

@Component("sales_OrderLineChangedListener")
public class OrderLineChangedListener {

    @Inject
    private TransactionalDataManager txDm;

    @EventListener
    public void beforeCommit(EntityChangedEvent<OrderLine, UUID> event) {
        Order order;
        if (event.getType() != EntityChangedEvent.Type.DELETED) { (1)
            order = txDm.load(event.getEntityId()) (2)
                    .view("orderLine-with-order") (3)
                    .one()
                    .getOrder(); (4)
        } else {
            Id<Order, UUID> orderId = event.getChanges().getOldReferenceId("order"); (5)
            order = txDm.load(orderId).one();
        }

        long count = txDm.load(OrderLine.class) (6)
                .query("select o from sales_OrderLine o where o.order = :order")
                .parameter("order", order)
                .view("orderLine-with-product")
                .list().stream()
                .filter(orderLine -> Boolean.TRUE.equals(orderLine.getProduct().getSpecial()))
                .count();

        order.setNumberOfSpecialProducts((int) count);

        txDm.save(order); (7)
    }
}
1 - если экземпляр OrderLine не удален, можно загрузить его из БД по идентификатору.
2 - метод event.getEntityId() возвращает id измененного экземпляра OrderLine.
3 - используем представление, которое содержит OrderLine вместе с Order, которому он принадлежит. Представление должно содержать атрибут Order.numberOfSpecialProducts, так как его необходимо будет обновить.
4 - получаем Order из загруженного OrderLine.
5 - если экземпляр OrderLine был только что удален, его нельзя загрузить из БД, но метод event.getChanges() возвращает все атрибуты удаленной сущности, включая идентификаторы связанных сущностей. Поэтому можно загрузить связанный Order по его id.
6 - загружаем все экземпляры OrderLine для данного Order, отфильтровываем по Product.special и считаем их. Представление должно содержать OrderLine вместе со связанным Product.
7 - сохраняем Order после изменения его атрибута.