Работа с 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 Обозначение хранилища данных, в котором находятся запрашиваемые сущности. Данный метод можно опустить, если сущность находится в основном хранилище.

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

При загрузке сущностей с использованием методов 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)
    );
}

Security in DataManager

DataManager performs the entity policy checks:

  • If the user has no rights to CREATE, UPDATE or DELETE action, the save() and remove() methods will throw the io.jmix.core.security.AccessDeniedException exception on the corresponding operation.

  • If the user has no rights to the READ action, the load() methods will return the empty result: null or an empty list. This applies only to the root entity of the loaded object graph; the references are always loaded.

DataManager also respects the row-level policies. The JPQL policy affects only the root entity of the loaded object graph, while the predicate policy affects the root entity and all linked entities.

The UnconstrainedDataManager interface has the same methods as DataManager but it doesn’t check the security policies. You can use it instead of the DataManager to bypass the security in your code, for example:

@Autowired
private UnconstrainedDataManager unconstrainedDataManager;

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

See more information about security in the Data Access Checks section.