Работа с DataManager

DataManager – это основной интерфейс для CRUD (Create, Read, Update, Delete) операций с сущностями. Он позволяет загружать графы сущностей по идентификатору или запросу, сохранять измененные экземпляры или удалять их. Вы можете использовать слушатели событий сущностей для выполнения действий по загрузке и сохранению определенных сущностей. DataManager поддерживает ссылки между сущностями из разных хранилищ данных как для JPA и DTO, так и для смешанных графов сущностей.

Вы можете инжектировать DataManager в бин Spring или контроллер экрана, например:

@Component
public class CustomerService {

    @Autowired
    private DataManager dataManager;

В приведенных ниже примерах переменная dataManager является ссылкой на DataManager.

Загрузка сущностей

DataManager предоставляет fluent API для загрузки сущностей. Используйте методы load(), принимающие класс сущности или Id, в качестве точек входа в этот API.

Загрузка сущности по идентификатору

Следующий метод загружает сущность по значению ее идентификатора:

Customer loadById(UUID customerId) {
    return dataManager.load(Customer.class) (1)
            .id(customerId)                 (2)
            .one();                         (3)
}
1 Точка входа в fluent loader API.
2 Метод id() принимает значение идентификатора.
3 Метод one() загружает экземпляр сущности. Если нет сущности с заданным идентификатором, метод выдает исключение IllegalStateException.

Идентификатор также может быть указан с помощью класса Id<E>, который содержит информацию о типе сущности. Тогда в прикладном коде не нужно использовать конкретный тип идентификатора сущности (UUID, Long и т.п.), и код загрузки становится еще короче:

Customer loadByGenericId(Id<Customer> customerId) {
    return dataManager.load(customerId).one();
}

Если сущность с заданным идентификатором может не существовать, вместо терминального метода one() используйте optional(), который возвращает Optional<E>. В приведенном ниже примере если сущность не найдена, создается и возвращается новый экземпляр:

Customer loadOrCreate(UUID customerId) {
    return dataManager.load(Customer.class)
            .id(customerId)
            .optional() (1)
            .orElse(dataManager.create(Customer.class));
}
1 Возвращает Optional<Customer>.

Вы также можете загрузить список сущностей по их идентификаторам, переданным в метод ids(), например:

List<Customer> loadByIds(UUID id1, UUID id2) {
    return dataManager.load(Customer.class)
            .ids(id1, id2)
            .list();
}

Сущности в списке результатов будут в том же порядке, что и предоставленные идентификаторы.

Загрузка всех сущностей

Следующий метод загружает все экземпляры сущности в список:

List<Customer> loadAll() {
    return dataManager.load(Customer.class).all().list();
}
Загружайте все экземпляры только в том случае, если вы уверены, что количество строк в соответствующей таблице всегда будет небольшим. В противном случае используйте запрос, условия и/или пейджинг.

Загрузка сущностей по запросу

При работе с реляционными базами данных используйте запросы на JPQL для загрузки данных. В разделе Расширения JPQL приведена информация о том, чем JPQL в Jmix отличается от стандартного JPA. Также обратите внимание, что DataManager может выполнять только запросы "select".

Следующий метод загружает список сущностей, используя полный запрос на JPQL и два параметра:

List<Customer> loadByFullQuery() {
    return dataManager.load(Customer.class)
            .query("select c from sample_Customer c where c.email like :email and c.grade = :grade")
            .parameter("email", "%@company.com")
            .parameter("grade", CustomerGrade.PLATINUM)
            .list();
}

Метод query() fluent-интерфейса принимает строку запроса как в полном, так и в сокращенном формате. Сокращенный запрос формируется следующим образом:

  • Выражение "select <alias>" всегда можно опустить.

  • Если выражение "from" содержит одну сущность, и вам не нужен особенный алиас, то выражение "from <entity> <alias> where" можно опустить. В этом случае фреймворк будет использовать алиас e.

  • Можно использовать позиционные параметры и передавать их значения прямо в метод query() в дополнительных аргументах.

Ниже приведен сокращенный эквивалент предыдущего примера:

List<Customer> loadByQuery() {
    return dataManager.load(Customer.class)
            .query("e.email like ?1 and e.grade = ?2", "%@company.com", CustomerGrade.PLATINUM)
            .list();
}

Пример более сложного сокращенного запроса с join:

List<Order> loadOrdersByProduct(String productName) {
    return dataManager.load(Order.class)
            .query("from sample_Order o, sample_OrderLine l " +
                    "where l.order = o and l.product.name = ?1", productName)
            .list();
}

Загрузка сущностей по условиям

Вы можете использовать условия вместо запроса на JPQL для фильтрации результатов. Например:

List<Customer> loadByConditions() {
    return dataManager.load(Customer.class)
            .condition(                                                      (1)
                LogicalCondition.and(                                        (2)
                    PropertyCondition.contains("email", "@company.com"),     (3)
                    PropertyCondition.equal("grade", CustomerGrade.PLATINUM) (3)
                )
            )
            .list();
}
1 Метод condition() принимает одно корневое условие.
2 Метод LogicalCondition.and() создает условие AND с заданными вложенными условиями.
3 Условия свойства сравнивают атрибуты сущности с указанными значениями.

Если вам нужно одно условие свойства, передайте его непосредственно в метод condition():

List<Customer> loadByCondition() {
    return dataManager.load(Customer.class)
            .condition(PropertyCondition.contains("email", "@company.com"))
            .list();
}

PropertyCondition позволяет указать свойства ссылаемых сущностей, например:

List<Order> loadByCondition() {
    return dataManager.load(Order.class)
            .condition(PropertyCondition.contains("customer.email", "@company.com"))
            .list();
}

Загрузка скалярных и агрегатных значений

Помимо экземпляров сущностей, DataManager может загружать скалярные и агрегатные значения в виде сущностей Key-Value.

Метод loadValues(String query) загружает список экземпляров KeyValueEntity, заполненный результатами данного запроса. Например:

String getCustomerPurchases(LocalDate fromDate) {
    List<KeyValueEntity> kvEntities = dataManager.loadValues(
            "select o.customer, sum(o.amount) from sample_Order o " +
                    "where o.date >= :date group by o.customer")
            .store("main")                      (1)
            .properties("customer", "sum")      (2)
            .parameter("date", fromDate)
            .list();

    StringBuilder sb = new StringBuilder();
    for (KeyValueEntity kvEntity : kvEntities) {
        Customer customer = kvEntity.getValue("customer");  (3)
        BigDecimal sum = kvEntity.getValue("sum");          (3)
        sb.append(customer.getName()).append(" : ").append(sum).append("\n");
    }
    return sb.toString();
}
1 Обозначение хранилища данных, в котором находятся запрашиваемые сущности. Данный метод можно опустить, если сущность находится в основном хранилище.
2 Перечисление имен атрибутов результирующей сущности Key-Value. Порядок имен должен соответствовать колонкам результирующего набора в запросе.
3 Получение загруженных значений из атрибутов сущности Key-Value.

Метод loadValue(String query, Class valueType) загружает одно значение заданного типа с помощью запроса. Например:

BigDecimal getTotal(LocalDate toDate) {
    return dataManager.loadValue(
                "select sum(o.amount) from sample_Order o where o.date >= :date",
                BigDecimal.class    (1)
            )
            .store("main")          (2)
            .parameter("date", toDate)
            .one();
}
1 Тип возвращаемого значения.
2 Обозначение хранилища данных, в котором находятся запрашиваемые сущности. Данный метод можно опустить, если сущность находится в основном хранилище.

Ограничения

Методы loadValues() и loadValue() имеют следующие ограничения:

  1. Запрос должен быть валидным JPQL запросом для одного указанного хранилища данных. Он может содержать только персистентные атрибуты JPA-сущностей.

  2. Ссылки между сущностями из разных хранилищ не поддерживаются.

  3. Данные методы не реализованы в REST DataStore.

Пейджинг и сортировка

При загрузке сущностей с использованием методов all(), query() или condition() результаты можно сортировать и разбивать на страницы.

Используйте методы firstResult() и maxResults() для пейджинга:

List<Customer> loadPageByQuery(int offset, int limit) {
    return dataManager.load(Customer.class)
            .query("e.grade = ?1", CustomerGrade.BRONZE)
            .firstResult(offset)
            .maxResults(limit)
            .list();
}

Используйте метод sort() для сортировки результатов:

List<Customer> loadSorted() {
    return dataManager.load(Customer.class)
            .condition(PropertyCondition.contains("email", "@company.com"))
            .sort(Sort.by("name"))
            .list();
}

В методе Sort.by() можно указать свойства ссылаемых сущностей, например:

List<Order> loadSorted() {
    return dataManager.load(Order.class)
            .all()
            .sort(Sort.by("customer.name"))
            .list();
}

При загрузке с помощью запроса на JPQL можно использовать стандартное выражение order by в запросе:

List<Customer> loadByQuerySorted() {
    return dataManager.load(Customer.class)
            .query("e.grade = ?1 order by e.name", CustomerGrade.BRONZE)
            .list();
}

Блокировка

Метод lockMode() с параметром из перечисления javax.persistence.LockModeType используется для задания блокировки JPA сущностей на уровне базы данных. В следующем примере показано получение пессимистичной блокировки при выполнении SQL запроса вида select …​ for update:

List<Customer> loadAndLock() {
    return dataManager.load(Customer.class)
            .query("e.email like ?1", "%@company.com")
            .lockMode(LockModeType.PESSIMISTIC_WRITE)
            .list();
}

Сохранение сущностей

Используйте метод save() для сохранения новых и измененных сущностей в базу данных.

В своей простейшей форме этот метод принимает экземпляр сущности и возвращает сохраненный экземпляр:

Customer saveCustomer(Customer entity) {
    return dataManager.save(entity);
}
Обычно переданные и возвращаемые экземпляры не совпадают. На возвращаемый экземпляр могут повлиять слушатели событий сущности, триггеры базы данных или права доступа. Поэтому, если вам нужно сохранить сущность, а затем продолжить работу с ней, всегда используйте экземпляр, возвращенный методом save().

Метод save() может принимать несколько экземпляров одновременно. В этом случае он возвращает объект EntitySet, из которого можно легко получить сохраненные экземпляры. В приведенном ниже примере создаются и сохраняются две связанные сущности и возвращается одна из них:

Order createOrderWithCustomer() {
    Customer customer = dataManager.create(Customer.class);
    customer.setName("Alice");

    Order order = dataManager.create(Order.class);
    order.setCustomer(customer);

    EntitySet savedEntities = dataManager.save(order, customer); (1)

    return savedEntities.get(order); (2)
}
1 Сохранение двух связанных сущностей. Порядок параметров save() не имеет значения.
2 Метод EntitySet.get() позволяет получить сохраненный экземпляр по его исходному экземпляру.

Наиболее универсальная форма метода save() принимает объект SaveContext, который можно использовать для добавления нескольких экземпляров и указания дополнительных параметров сохранения. В приведенном ниже примере коллекция сущностей сохраняется с использованием SaveContext:

EntitySet saveUsingContext(List<Customer> entities) {
    SaveContext saveContext = new SaveContext();
    for (Customer entity : entities) {
        saveContext.saving(entity);
    }
    return dataManager.save(saveContext);
}

Производительность операции сохранения

Существует несколько методик улучшения производительности операции сохранения. Они особенно полезны при работе с большими коллекциями сущностей.

Во первых, вместо передачи каждого экземпляра в метод save(entity) по отдельности, рекомендуется сохранять все экземпляры в одной транзакции (если коллекция не слишком большая, не более 1000). Это можно сделать, добавляя экземпляры в SaveContext и используя метод save(SaveContext), как было рассмотрено в предыдущем разделе.

Если сохраненные экземпляры не нужны вам немедленно, используйте метод SaveContext.setDiscardSaved(true). Это повысит производительность, поскольку DataManager не будет извлекать сохраненные cущности из базы данных. Например:

void saveAndReturnNothing(List<Customer> entities) {
    // create SaveContext and set its 'discardSaved' property
    SaveContext saveContext = new SaveContext().setDiscardSaved(true);
    for (Customer entity : entities) {
        saveContext.saving(entity);
    }
    dataManager.save(saveContext);
}

Если не требуется проверка разрешений безопасности для текущего пользователя, можно получить дополнительный выигрыш в производительности используя UnconstrainedDataManager. Например:

void saveByUnconstrainedDataManager(List<Customer> entities) {
    SaveContext saveContext = new SaveContext().setDiscardSaved(true);
    for (Customer entity : entities) {
        saveContext.saving(entity);
    }
    // use 'UnconstrainedDataManager' which bypasses security
    dataManager.unconstrained().save(saveContext);
}

Если коллекция большая (более 1000 экземпляров), нужно обязательно разбивать операцию сохранения на пакеты. Например:

void saveInBatches(List<Customer> entities) {
    SaveContext saveContext = new SaveContext().setDiscardSaved(true);
    for (int i = 0; i < entities.size(); i++) {
        saveContext.saving(entities.get(i));
        // save by 100 instances
        if ((i + 1) % 100 == 0 || i == entities.size() - 1) {
            dataManager.save(saveContext);
            saveContext = new SaveContext().setDiscardSaved(true);
        }
    }
}

См. сравнение различных методов сохранения сущностей в проекте jmix-data-performance-tests на GitHub.

Удаление сущностей

Используйте метод remove() для удаления сущностей из базы данных.

В своей простейшей форме данный метод принимает экземпляр сущности для удаления:

void removeCustomer(Customer entity) {
    dataManager.remove(entity);
}

Также метод remove() может принимать несколько экземпляров, массивов и коллекций:

void removeCustomers(List<Customer> entities) {
    dataManager.remove(entities);
}

Если вы удаляете связанные объекты, может быть важен порядок параметров. Сначала передайте сущности, которые зависят от других, например:

void removeOrderWithCustomer(Order order) {
    dataManager.remove(order, order.getCustomer());
}

Если у вас нет экземпляра сущности, а есть только его идентификатор, создайте объект Id из идентификатора и передайте его методу remove():

void removeCustomer(UUID customerId) {
    dataManager.remove(Id.of(customerId, Customer.class));
}

Если вам необходимо указать дополнительные параметры операции удаления, например, чтобы отключить мягкое удаление и полностью удалить из базы данных сущность с чертой Soft Delete, используйте метод save() с SaveContext и передайте удаленные сущности его методу removing():

void hardDelete(Product product) {
    dataManager.save(
            new SaveContext()
                    .removing(product)
                    .setHint(PersistenceHints.SOFT_DELETION, false)
    );
}

Транзакции в DataManager

При работе с JPA сущностями DataManager по умолчанию использует текущую транзакцию или создает и коммитит новую, если текущей транзакции нет.

Управлять транзакциями можно используя аннотации или TransactionTemplate, как описано в разделе Управление транзакциями.

Кроме того, DataManager позволяет контролировать свое собственное внутреннее транзакционное поведение.

При загрузке данных с использованием fluent API можно использовать метод joinTransaction(false), чтобы начать и закоммитить отдельную транзакцию для данной операции:

Customer loadCustomerInSeparateTransaction(UUID customerId) {
    return dataManager.load(Customer.class)
            .id(customerId)
            .joinTransaction(false)
            .one();
}

При сохранении сущностей используйте метод setJoinTransaction(false) класса SaveContext, чтобы начать и закоммитить отдельную транзакцию для операции с этим контекстом:

void saveCustomerInSeparateTransaction(Customer entity) {
    SaveContext saveContext = new SaveContext().saving(entity)
            .setJoinTransaction(false);
    dataManager.save(saveContext);
}

Безопасность в DataManager

DataManager выполняет проверки политики сущностей:

  • Если у пользователя нет прав на действия CREATE, UPDATE или DELETE, методы save() и remove() выбросят исключение io.jmix.core.security.AccessDeniedException при соответствующей операции.

  • Если у пользователя нет прав на действие READ, методы load() вернут пустой результат: null или пустой список. Это касается только корневой сущности загруженного графа объектов; ссылки загружаются всегда.

DataManager также учитывает политики уровня строк. JPQL-политика влияет только на корневую сущность загруженного графа объектов, в то время как предикатная политика влияет на корневую сущность и все связанные сущности.

Интерфейс UnconstrainedDataManager имеет те же методы, что и DataManager, но не проверяет политики безопасности. Его можно использовать вместо DataManager, чтобы обойти проверки безопасности в вашем коде, например:

@Autowired
private UnconstrainedDataManager unconstrainedDataManager;

public Customer loadByIdUnconstrained(UUID customerId) {
    return unconstrainedDataManager.load(Customer.class)
            .id(customerId)
            .one();
}

См. дополнительную информацию о безопасности в разделе Проверки доступа к данным.