Кастомные сервисы обновления

Распространённый способ инкапсулировать бизнес-логику, выполняемую при сохранении или удалении сущности, — поместить её в сервис и вызывать этот сервис из своего кода. Поток выполнения последовательный, его легко читать и отлаживать. Это хорошо работает, когда вы вызываете сервис сами, но универсальные механизмы фреймворка — такие как универсальный REST, инспектор сущностей и BPM entity data task — сохраняют и удаляют сущности через DataManager и ничего не знают о вашем сервисе.

Кастомные сервисы обновления решают эту проблему. Реализуя интерфейсы SaveDelegate и RemoveDelegate, сервис регистрирует себя как обработчик операций сохранения и удаления для определённого типа сущности. После этого универсальные механизмы фреймворка вызывают ваш сервис вместо DataManager, поэтому ваша бизнес-логика выполняется единообразно независимо от того, откуда инициирована операция.

Это альтернатива использованию событий сущностей для логики сохранения/удаления. По сравнению со слушателями событий, сервис обновления держит весь поток в одном месте и выполняет его последовательно, что проще отслеживать, чем цепочку слушателей. Простые сквозные задачи, такие как аудит или отправка уведомлений, по-прежнему хорошо подходят для событий сущностей.

Эта возможность является экспериментальной. Интерфейсы SaveDelegate и RemoveDelegate помечены аннотацией @Experimental и могут измениться в будущих релизах.

Делегаты сохранения и удаления

Фреймворк предоставляет два интерфейса в пакете io.jmix.core:

  • SaveDelegate<E> объявляет метод, вызываемый вместо DataManager при сохранении сущности типа E:

    E save(E entity, SaveContext saveContext);
  • RemoveDelegate<E> объявляет метод, вызываемый вместо DataManager при удалении сущности типа E:

    void remove(E entity);

Кастомный сервис, обрабатывающий обновления определённой сущности, реализует один или оба этих интерфейса, параметризованных классом сущности. Фреймворк определяет подходящий сервис по типу сущности, поэтому на каждый класс сущности должно приходиться не более одного бина, реализующего SaveDelegate (и одного, реализующего RemoveDelegate).

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

Создание сервиса обновления

Давайте создадим сервис, который обрабатывает сохранение и удаление сущности Order. При сохранении заказа он пересчитывает общую сумму заказа на основе его строк, а для нового заказа увеличивает количество заказов связанного покупателя. При удалении заказа он уменьшает это количество.

Для хранения количества заказов сущность Customer имеет дополнительный атрибут:

@Column(name = "ORDERS_COUNT")
private Integer ordersCount;

Сервис реализует оба интерфейса SaveDelegate<Order> и RemoveDelegate<Order>:

@Component
public class OrderUpdateService implements SaveDelegate<Order>, RemoveDelegate<Order> {

    @Autowired
    private DataManager dataManager;
    @Autowired
    private CustomerRepository customerRepository;
    @Autowired
    private EntityStates entityStates;

    @Override
    @Transactional
    public Order save(Order order, SaveContext saveContext) {   (1)
        calculateTotalAmount(order);                            (2)
        if (entityStates.isNew(order)) {                        (3)
            incrementCustomerOrdersCount(order);
        }
        return dataManager.save(saveContext).get(order);        (4)
    }

    @Override
    @Transactional
    public void remove(Order order) {                          (5)
        decrementCustomerOrdersCount(order);
        dataManager.remove(order);
    }
1 Метод save() вызывается как явно из вашего собственного кода, так и неявно универсальными механизмами фреймворка.
2 Бизнес-логика, обновляющая состояние сущности перед сохранением.
3 Используйте EntityStates, чтобы применять логику только для новых экземпляров.
4 Сохраните сущность. Здесь мы передаём входящий saveContext в DataManager, чтобы заказ и его строки композиции сохранялись вместе. Вы также можете сохранить через репозиторий данных.
5 Метод remove() вызывается как явно из вашего кода, так и неявно фреймворком.

Методы бизнес-логики пересчитывают сумму и поддерживают количество заказов покупателя:

private void calculateTotalAmount(Order order) {
    if (order.getLines() != null) {
        BigDecimal total = order.getLines().stream()
                .map(this::getLineTotal)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        order.setAmount(total);
    }
}

private BigDecimal getLineTotal(OrderLine line) {
    if (line.getProduct() == null || line.getQuantity() == null) {
        return null;
    }
    return line.getProduct().getPrice()
            .multiply(BigDecimal.valueOf(line.getQuantity()));
}
private void incrementCustomerOrdersCount(Order order) {
    // the related entity is reloaded because the instance held by
    // the order can be stale
    customerRepository.findById(order.getCustomer().getId()).ifPresent(customer -> {
        customer.setOrdersCount(getCurrentOrdersCount(customer) + 1);
        customerRepository.save(customer);
    });
}

private void decrementCustomerOrdersCount(Order order) {
    customerRepository.findById(order.getCustomer().getId()).ifPresent(customer -> {
        customer.setOrdersCount(getCurrentOrdersCount(customer) - 1);
        customerRepository.save(customer);
    });
}

private static int getCurrentOrdersCount(Customer customer) {
    return customer.getOrdersCount() == null ? 0 : customer.getOrdersCount();
}

Несколько моментов, на которые стоит обратить внимание:

  • Аннотируйте методы save() и remove() аннотацией @Transactional, чтобы все операции с хранилищем данных выполнялись в одной транзакции.

  • Связанную сущность, которую вы намереваетесь изменить (в этом примере — Customer), следует перезагрузить внутри сервиса, поскольку экземпляр, на который ссылается сохраняемая сущность, может быть устаревшим.

  • Сервис использует репозиторий данных для загрузки и сохранения связанного Customer, но вы также можете использовать DataManager напрямую.

Использование сервисов обновления в вашем коде

После создания сервиса универсальные механизмы фреймворка (универсальный REST, инспектор сущностей, BPM entity data task) автоматически направляют операции сохранения и удаления сущности Order в него.

В вашем собственном коде следует последовательно использовать сервис для сохранения и удаления сущностей, для которых он определён, вместо прямого вызова DataManager или репозитория данных. В противном случае бизнес-логика, инкапсулированная в сервисе, будет обойдена.

В экранах подключайте сервис к компонентам данных через стандартные обработчики-делегаты сохранения и удаления, так же как для репозиториев данных.

Studio помогает создавать сервисы обновления и использовать их в экранах. При создании JPA-сущности установите флажок Create Update Service в диалоге New JPA Entity, чтобы сгенерировать класс сервиса, реализующий SaveDelegate и RemoveDelegate. Его методы-делегаты вызывают либо DataManager, либо репозиторий данных, если он также создан. Позже, при создании экрана для сущности, имеющей сервис обновления, установите флажок Use Update Service в диалоге Create Jmix View, чтобы автоматически делегировать этому сервису сохранение и удаление.

Ограничения

  • Операции загрузки не делегируются. Фреймворк делегирует кастомным сервисам только операции сохранения и удаления. Если вам нужно выполнять логику при загрузке сущности — например, инициализировать неперсистентные атрибуты — используйте слушатель EntityLoadingEvent, как описано в разделе События сущностей. Загрузка также остаётся под вашим контролем в вашем собственном коде: используйте запрос, репозиторий данных или любой метод сервиса для загрузки сущностей.

  • Согласованность — ваша ответственность. Фреймворк не может гарантировать, что код приложения всегда проходит через сервис. Убедитесь, что везде в вашем коде сервис сущности используется для её операций сохранения и удаления, а не DataManager или репозиторий данных напрямую.