События сущностей

Когда вы сохраняете и загружаете сущности с помощью DataManager, подсистема доступа к данным Jmix посылает определенные Spring application events. Вы можете создать слушателя событий для выполнения дополнительных действий с сохраненными или загруженными экземплярами сущностей.

Использование EntityChangedEvent

EntityChangedEvent посылается фреймворком при сохранении экземпляра сущности в базе данных. Вы можете обрабатывать событие как внутри транзакции, так и после ее завершения. В обоих случаях на момент события база данных уже содержит измененные данные.

EntityChangedEvent содержит тип изменения (создание, обновление или удаление), идентификатор измененной сущности, информацию о том, какие атрибуты были изменены, и старые значения измененных атрибутов. Для ссылочных атрибутов старые значения содержат идентификаторы ссылаемых сущностей.

Обработка изменений до коммита

Чтобы обработать EntityChangedEvent в текущей транзакции, создайте метод бина с аннотацией @EventListener. Метод будет вызван фреймворком сразу после сохранения сущности в базе данных, но до коммита транзакции. Вы можете вносить любые изменения в данные в методе-слушателе, и они будут закоммичены вместе с первоначальными изменениями. Если произойдет исключение, все изменения будут отменены.

В приведенном ниже примере создается связанная сущность для регистрации изменения атрибута. Как измененный 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 Получение идентификатора измененной сущности.
4 Получение старого значения измененного атрибута.
5 Загрузка нового состояния измененной сущности.

Давайте рассмотрим другой пример. Здесь атрибут amount сущности Order обновляется всякий раз, когда создается, обновляется или удаляется один из ее экземпляров OrderLine`:

@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 После удаления сущности больше нельзя загрузить ее экземпляр, поэтому используются старые значения, чтобы получить ссылку на соответствующий Order.
2 Используйте getOldReference() и getOldCollection() вместо getOldValue() для ссылочных атрибутов to-one и to-many.

Обработка изменений после коммита

Чтобы обработать EntityChangedEvent после сохранения изменений в базе данных и коммита транзакции, создайте метод бина с аннотацией @TransactionalEventListener.

Следует иметь в виду, что исключения, выброшенные в слушателе "after commit" не передаются в вызывающий код и не логгируются. Поэтому рекомендуется оборачивать код слушателя в try-catch.

Если вам нужно загрузить или сохранить какие-либо данные в слушателе событий "after commit", всегда начинайте новую транзакцию.

В приведенном ниже примере демонстрируется обработка исключений и загрузка сущности с помощью 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);
        }
    }

Если вам нужно сохранить сущность в слушателе "after commit", используйте SaveContext и его метод setJoinTransaction(false), например:

dataManager.save(new SaveContext()
        .saving(entity)
        .setJoinTransaction(false)
);

Если у вас есть несколько вызовов DataManager или других сервисов, которые могут потребовать наличие транзакции, начните новую транзакцию для всего метода, например:

@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW) (1)
void onCustomerChangedAfterCommit2(EntityChangedEvent<Customer> event) {
    // ...
}
1 Propagation.REQUIRES_NEW здесь необходим для запуска новой транзакции.

Использование EntitySavingEvent и EntityLoadingEvent

EntitySavingEvent посылается фреймворком прямо перед сохранением экземпляра сущности в базе данных. В отличие от 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);
    }