События сущностей
Когда вы сохраняете и загружаете сущности с помощью 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 в отдельной транзакции:
@Component
public class CustomerEventListener {
    private static final Logger log = LoggerFactory.getLogger(CustomerEventListener.class);
    @Autowired
    private DataManager dataManager;
    @TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW) (1)
    void onCustomerChangedAfterCommit(EntityChangedEvent<Customer> event) {
        try {
            if (event.getType() != EntityChangedEvent.Type.DELETED
                    && event.getChanges().isChanged("grade")) {
                Customer customer = dataManager.load(event.getEntityId()).one();
                emailCustomerTheirNewGrade(customer.getEmail(), customer.getGrade());
            }
        } catch (Exception e) {
            log.error("Error handling Customer changes after commit", e);
        }
    }| 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);
    }