3.4.7. EntityChangedEvent

EntityChangedEvent is a Spring’s ApplicationEvent which is sent by the framework on the middle tier when an entity instance is saved to the database. The event can be handled both inside the transaction and after its completion (using @TransactionalEventListener).

The event is sent only for entities annotated with @PublishEntityChangedEvents. Do not forget to add this annotation to entities for which you want to listen to EntityChangedEvent.

EntityChangedEvent does not contain the changed entity instance but only its id. Also, the getOldValue(attributeName) method returns ids of references instead of objects. So if needed, the developer should reload entities with a required view and other parameters.

Below is an example of handling the EntityChangedEvent for a Customer entity in the current transaction and after its completion:

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.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.UUID;

@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {

    @TransactionalEventListener(
            phase = TransactionPhase.BEFORE_COMMIT (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(
            phase = TransactionPhase.AFTER_COMMIT (6)
    )
    public void afterCommit(EntityChangedEvent<Customer, UUID> event) {
        (7)
    }
}
1 - this listener is invoked inside the current transaction.
2 - changed entity’s id.
3 - change type: CREATED, UPDATED or DELETED.
4 - you can check if a particular attribute has been changed.
5 - you can get the old value of a changed attribute.
6 - this listener is invoked after the transaction is committed.
7 - after transaction commit, the event contains the same information as before commit.

If the listener is invoked inside the transaction, you can roll it back by throwing an exception. Nothing will be saved in the database then. If you don’t want any notifications to the user, use SilentException.

If an "after commit" listener throws an exception, it will be logged, but not propagated to the client (the user won’t see the error in UI).

When handling EntityChangedEvent in the current transaction (TransactionPhase.BEFORE_COMMIT), make sure you are using TransactionalDataManager to get the current state of the changed entity from the database. If you use DataManager, it will create a new transaction which may lead to a deadlock in the database if you try to read non-committed data.

In "after commit" listeners (TransactionPhase.AFTER_COMMIT), use DataManager or explicitly create a new transaction before using TransactionalDataManager.

Below is an example of using EntityChangedEvent to update related entities.

Suppose we have Order, OrderLine and Product entities as in the Quick Start Sales Application, but Product additionally has special boolean attribute and Order has numberOfSpecialProducts integer attribute which should be recalculated each time an OrderLine is created or deleted from the Order.

Create the following class with the @EventListener method which will be invoked for changed OrderLine entities before transaction commit:

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;

    @TransactionalEventListener(
            phase = TransactionPhase.BEFORE_COMMIT
    )
    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 - if OrderLine is not deleted, we can load it from the database by id.
2 - event.getEntityId() method returns id of the changed OrderLine.
3 - use a view that contains OrderLine together with the Order it belongs to. The view must contain the Order.numberOfSpecialProducts attribute because we need to update it later.
4 - get Order from the loaded OrderLine.
5 - if OrderLine has just been deleted, it cannot be loaded from the database, but event.getChanges() method returns all attributes of the entity, including identifiers of related entities. So we can load related Order by its id.
6 - load all OrderLine instances for the given Order, filter them by Product.special and count them. The view must contain OrderLine together with the related Product.
7 - save Order after changing its attribute.