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