Работа с 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() имеют следующие ограничения:
- 
Запрос должен быть валидным JPQL запросом для одного указанного хранилища данных. Он может содержать только персистентные атрибуты JPA-сущностей. 
- 
Ссылки между сущностями из разных хранилищ не поддерживаются. 
- 
Данные методы не реализованы в 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();
}См. дополнительную информацию о безопасности в разделе Проверки доступа к данным.