使用 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 的具体类型(UUIDLong 等)了,加载实体的代码也更加简洁:

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 指定数据所在的数据存储。如果实体在主数据存储,则可以省略该方法。

局限性

loadValues()loadValue() 方法有下列限制:

  1. 查询语句必须是单一指定数据存储的有效 JPQL。只能包含 JPA 实体的持久化属性。

  2. 不支持 跨数据存储的引用

  3. 这些方法没有在 REST 数据存储 中实现。

分页和排序

当使用 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 项目了解不同保存实体方式的性能比较。项目中测试了 DataManagerEntityManagerJdbcTemplate 单条插入以及批量插入的性能对比,并且在 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 参数,并将要删除的实体传给 SaveContextremoving() 方法:

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();
}

当保存实体时,使用 SaveContextsetJoinTransaction(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 部分。