Entity Events

When you save and load entities using DataManager, the Jmix data access subsystem sends specific Spring application events. You can create event listeners to perform some additional actions with saved or loaded entity instances.

Using EntityChangedEvent

EntityChangedEvent is sent by the framework when an entity instance is saved to the database. You can handle the event both inside the transaction and after its completion. In both cases, at the time of the event, the database already contains the changed data.

EntityChangedEvent contains change type (create, update or delete), the identifier of the changed entity, the information on what attributes were changed, and old values of changed attributes. For reference attributes, the old values contain identifiers of the referenced entities.

Handling Changes Before Commit

To handle EntityChangedEvent in the current transaction, create a bean method annotated with @EventListener. The method will be called by the framework right after saving the entity to the database but before the transaction commit. In the listener method, you can make any changes to data and they will be committed together with the initial changes. If an exception occurs, everything will be rolled back.

In the example below, we create a linked entity to register the change of an attribute. Both the changed Customer and the created CustomerGradeChange instances will be committed in the same transaction:

@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 Determining the change type.
2 Checking if an attribute was actually changed.
3 Getting the changed entity id.
4 Obtaining an old value of the changed attribute.
5 Loading the new state of the changed entity.

Let’s look at another example. Here the amount attribute of the Order entity is updated whenever one of its OrderLine instances is created, updated or deleted:

@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 When the entity is deleted, we cannot load its instance anymore, so we use old values to get the reference to the enclosing Order.
2 Use getOldReference() and getOldCollection() instead of getOldValue() for to-one and to-many reference attributes.

Handling Changes After Commit

To handle EntityChangedEvent after the changes are saved to the database and the transaction is committed, create a bean method annotated with @TransactionalEventListener.

Note that exceptions occurred in an "after commit" event listener are not propagated to the calling code and not logged. So it is recommended to wrap your code in the try-catch clause.

If you need to load or save any data in an "after commit" event listener, always start a new transaction.

The example below demonstrates exception handling and loading an entity with DataManager in a separate transaction (see joinTransaction(false) method):

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

If you need to save an entity in an "after commit" event listener, use SaveContext and its setJoinTransaction(false) method, for example:

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

If you have multiple calls to DataManager or to other services that may require a transaction, start the new transaction for the whole method, for example:

@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW) (1)
void onCustomerChangedAfterCommit(EntityChangedEvent<Customer> event) {
    // ...
}
1 Propagation.REQUIRES_NEW is necessary here for starting the new transaction.

Using EntitySavingEvent and EntityLoadingEvent

EntitySavingEvent is sent by the framework when an entity instance is about to be saved to the database. As opposed to EntityChangedEvent, which contains the entity id, EntitySavingEvent contains the entity instance itself. It allows you to change the instance state before it is saved in the database fields.

The event has the isNewEntity() method that returns true if the event is sent for a new instance that will be inserted to a database table.

An EntitySavingEvent listener can be used to initialize entity attributes before saving to the database. For example:

@Component
public class OrderEventListener {

    @EventListener
    void onOrderSaving(EntitySavingEvent<Order> event) {
        if (event.isNewEntity()) {
            Order order = event.getEntity();
            order.setNumber(generateOrderNumber());
        }
    }

EntityLoadingEvent is sent by the framework when an entity instance is loaded from the database. You can use it for initializing non-persistent attributes from the persistent state.

In the example below, EntitySavingEvent and EntityLoadingEvent listeners maintain an encrypted attribute:

@JmixEntity
@Table(name = "CUSTOMER")
@Entity(name = "sample_Customer")
public class Customer {

    @Column(name = "ENCRYPTED_DATA")
    @Lob
    private String encryptedData;

    @Transient
    @JmixProperty
    private String sensitiveData;

When the entity is saved, the sensitive content is encrypted and stored in the database. On loading, the content is decrypted and set back to the transient attribute to be accessible by users:

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