使用 DataManager
对实体进行 CRUD 操作的主要接口是 DataManager
。基本功能包括通过 ID 或查询语句加载实体关系图、保存实体和删除实体。可以用实体事件监听器在加载和保存特定实体时做一些额外的操作。DataManager
中也维护了跨数据库的实体引用,包括 JPA、DTO 和混合实体的实体关系图。
可以在 Spring bean 或界面控制器内注入 DataManager
,示例:
@Component
public class CustomerService {
@Autowired
private DataManager dataManager;
在下面的所有例子中,我们都省略定义的代码并默认 dataManager
变量就是 DataManager
。
加载实体
DataManager
提供加载实体的流式接口。流式接口的入口是使用 load()
方法,该方法可以接收实体类或者 Id
参数。
用 Id 加载实体
下面的方式使用实体的 id 加载实体:
Customer loadById(UUID customerId) {
return dataManager.load(Customer.class) (1)
.id(customerId) (2)
.one(); (3)
}
1 | 流式加载器 API 的入口。 |
2 | id() 接收 id 值。 |
3 | one() 方法加载实体实例。如果给定的 id 匹配不到实体,该方法会抛出 IllegalStateException 。 |
实体标识符还可以使用 Id<E>
类指定,该类包含实体类型的信息。代码中就不需要使用实体 id 的具体类型(UUID
、Long
等)了,加载实体的代码也更加简洁:
Customer loadByGenericId(Id<Customer> customerId) {
return dataManager.load(customerId).one();
}
如果给定 id 的实体可能不存在的话,除了用 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()
方法接收多个实体 id 加载实体列表,示例:
List<Customer> loadByIds(UUID id1, UUID id2) {
return dataManager.load(Customer.class)
.ids(id1, id2)
.list();
}
结果列表中的实体顺序与 id 传入的顺序一致。
加载所有实体
下面的方法将所有的实体加载到一个列表中:
List<Customer> loadAll() {
return dataManager.load(Customer.class).all().list();
}
用查询语句加载实体
当使用关系型数据库时,可以用 JPQL 查询语句加载实体。参阅 JPQL 扩展 了解 Jmix 中的 JPQL 与 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()
方法可以使用完整的或者简写的查询语句。如果使用简写的查询语句,则需要遵守下列规则:
-
可以省略
"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();
}
使用 Conditions 加载实体
除了 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()
方法可以在数据库层面对 JPA 实体加锁,该方法接收 jakarta.persistence.LockModeType
枚举类型值作为参数。下面的示例中,使用了悲观锁,最后形成的 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
不需要从数据库加载保存后的实体了。示例:
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);
}
}
}
可以参考 GitHub 的 jmix-data-performance-tests 项目了解不同保存实体方式的性能比较。项目中测试了 DataManager
、EntityManager
和 JdbcTemplate
单条插入以及批量插入的性能对比,并且在 test\java\com\company\demo
目录提供了相应的源码。
删除实体
用 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,那么可以从 id 构造一个 Id
对象并传给 remove()
方法:
void removeCustomer(UUID customerId) {
dataManager.remove(Id.of(customerId, Customer.class));
}
如需为删除操作设置额外的参数,例如,需要关掉 软删除,将带有软删除特性的实体完全删除,那么需要用 save()
方法,带 SaveContext
参数,并将要删除的实体传给 SaveContext
的 removing()
方法:
void hardDelete(Product product) {
dataManager.save(
new SaveContext()
.removing(product)
.setHint(PersistenceHints.SOFT_DELETION, false)
);
}
DataManager 中的事务
处理 JPA 实体时,DataManager
默认会使用当前的已有的事务,如果无可用的事务,则会创建一个新的事务并提交。
可以按照 事务管理 章节的说明使用注解或 TransactionTemplate
管理事务。
此外,DataManager
还支持控制其内部事务的行为。
当使用流式 API 加载数据时,可以用 joinTransaction(false)
方法为当前操作创建并提交一个单独的事务:
Customer loadCustomerInSeparateTransaction(UUID customerId) {
return dataManager.load(Customer.class)
.id(customerId)
.joinTransaction(false)
.one();
}
当保存实体时,使用 SaveContext
的 setJoinTransaction(false)
为当前 context 的操作发起并提交一个单独的事务:
void saveCustomerInSeparateTransaction(Customer entity) {
SaveContext saveContext = new SaveContext().saving(entity)
.setJoinTransaction(false);
dataManager.save(saveContext);
}
DataManager 安全机制
DataManager
会执行 实体策略 检查:
-
如果用户无权创建、更新或删除,则
save()
和remove()
方法会抛出io.jmix.core.security.AccessDeniedException
异常。 -
如果用户无权限读取,则
load()
方法会返回空结果:null
或空列表。这个规则只应用在加载根实体时,不影响子实体的加载。
DataManager
也会遵循 行级策略。JPQL 策略仅影响对象图中根实体的加载,而谓词策略会影响根实体和所有子实体的加载。
UnconstrainedDataManager
接口中的方法与 DataManager
相同,但是不会检查安全策略。在代码中可以用来绕过安全检查,示例:
@Autowired
private UnconstrainedDataManager unconstrainedDataManager;
public Customer loadByIdUnconstrained(UUID customerId) {
return unconstrainedDataManager.load(Customer.class)
.id(customerId)
.one();
}
关于安全的更多内容请参阅 security:authorization.adoc#data-access-checks 部分。