Работа с 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)
);
}
Транзакции в 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();
}
См. дополнительную информацию о безопасности в разделе Проверки доступа к данным.