数据加载器

数据加载器为 数据容器 加载数据。

根据交互的数据容器不同,数据加载器的接口有稍微的不同:

  • InstanceLoader 使用实体 id 或者 JPQL 查询语句加载单一实体到 InstanceContainer

  • CollectionLoader 使用 JPQL 查询语句加载实体集合到 CollectionContainer。可以设置分页、排序以及其他可选的参数。

  • KeyValueCollectionLoader 加载 KeyValueEntity 实体的集合到 KeyValueCollectionContainer。除了 CollectionLoader 参数,还可以指定一个数据存储名称。

在视图的 XML 描述中,所有的类型的加载器都是使用 <loader> 元素定义,加载器的类型通过归属的数据容器类型确定。

数据加载器不是必要的,因为可以使用 DataManager 或者自定义的服务来加载数据,然后直接设置给容器。但通过在视图中声明式的定义加载器可以简化数据加载的过程。

通常,集合加载器从视图的描述文件中获得 JPQL 查询语句,之后创建 LoadContext 并调用 DataManager 加载实体。所以,典型的 XML 描述看起来是这样:

<collection id="departmentsDc"
            class="com.company.onboarding.entity.Department">
    <fetchPlan extends="_base">
        <property name="hrManager" fetchPlan="_base"/>
    </fetchPlan>
    <loader id="departmentsDl">
        <query>
            <![CDATA[select e from Department e]]>
        </query>
    </loader>
</collection>

在实体详情视图中,加载器的 XML 元素通常是空的,因为实例加载器需要一个实体的标识符,这个标识符通过编程的方式使用 StandardDetailView 基类指定。

<instance id="departmentDc"
          class="com.company.onboarding.entity.Department">
    <fetchPlan extends="_base">
        <property name="hrManager" fetchPlan="_base"/>
    </fetchPlan>
    <loader id="departmentDl"/>
</instance>

当加载器设置了 DataContext(当使用 XML 描述定义加载器的时候是默认设置的),所有加载的实体都自动合并到数据上下文。

事件和处理器

本节介绍可以在视图控制器中处理的数据加载器生命周期事件。

如需使用 Jmix Studio 生成处理方法的桩代码,需要在视图 XML 描述或者 Jmix UI 层级结构面板选中元素,然后用 Jmix UI 组件面板的 Handlers 标签页生成。

或者可以使用视图控制器顶部面板的 Generate Handler 按钮。

loadDelegate

加载器可以将实际的加载动作代理给视图控制器的一个方法,方法中可以调用自定义服务,而不使用默认的 DataManager。示例:

@Autowired
private DepartmentService departmentService;

@Install(to = "departmentsDl", target = Target.DATA_LOADER) (1)
private List<Department> departmentsDlLoadDelegate(final LoadContext<Department> loadContext) { (2)
    LoadContext.Query query = loadContext.getQuery();
    return departmentService.loadDepartments( (3)
            query.getCondition(),
            query.getSort(),
            query.getFirstResult(),
            query.getMaxResults()
    );
}
1 departmentsDl 加载器会使用 departmentsDlLoadDelegate() 方法来加载 Department 实体列表
2 此方法接收 LoadContext 参数,加载器会按照其参数(查询语句、过滤器等等)来创建这个参数。
3 数据加载是通过 DepartmentService.loadDepartments() 来完成的,该方法接收视图可视化组件为加载器设置的过滤条件、排序和分页参数。

除了调用自定义服务之外,加载代理方法还支持对加载的实体进行加载后处理。

PreLoadEvent

该事件在加载实体之前发送。

@Subscribe(id = "departmentsDl", target = Target.DATA_LOADER)
public void onDepartmentsDlPreLoad(final CollectionLoader.PreLoadEvent<Department> event) {
    // some actions before loading
}

可以使用事件的 preventLoad() 方法阻止加载。

PostLoadEvent

该事件在成功加载实体、将实体合并到 DataContext 并设置到容器之后发出。

@Subscribe(id = "departmentsDl", target = Target.DATA_LOADER)
public void onDepartmentsDlPostLoad(final CollectionLoader.PostLoadEvent<Department> event) {
    // some actions after loading
}

查询条件

有时需要在运行时修改数据加载器的查询语句,以便在数据库级别过滤加载的数据。根据用户输入的参数进行过滤最简单的方法就是将 propertyFilter 属性过滤器genericFilter 通用过滤器 可视化组件与数据加载器关联起来。

无论是否使用全局过滤器,都可以为加载器查询语句单独创建一组过滤条件。一个过滤条件是一组带参数的查询子句。子句中所有的参数都设置了之后,这些子句才会被添加到生成的查询语句文本中。

过滤条件会在数据存储级别处理,因此可以包含各个数据存储支持的不同语言的子句。框架会自动处理 JPQL 的过滤条件。

下面例子中,按照 Department 实体的两个属性:namehrManager 对实体进行过滤。

加载器的查询过滤条件可以通过 <condition> XML 元素进行声明式的定义,或者通过 setCondition() 方法编程式的定义。下面是在 XML 中配置条件的示例:

<view xmlns="http://jmix.io/schema/flowui/view"
      xmlns:c="http://jmix.io/schema/flowui/jpql-condition"> (1)
    <data readOnly="true">
        <collection id="departmentsDc"
                    class="com.company.onboarding.entity.Department">
            <fetchPlan extends="_base">
                <property name="hrManager" fetchPlan="_base"/>
            </fetchPlan>
            <loader id="departmentsDl">
                <query>
                    <![CDATA[select e from Department e]]>
                    <condition> (2)
                        <and> (3)
                            <c:jpql> (4)
                                <c:where>e.name like :name</c:where>
                            </c:jpql>
                            <c:jpql>
                                <c:where>e.hrManager = :hrManager</c:where>
                            </c:jpql>
                        </and>
                    </condition>
                </query>
            </loader>
        </collection>
1 添加 JPQL condition 命名空间
2 query 内定义 condition 元素
3 如果有多个条件,添加 andor 元素
4 使用可选的 join 元素和必需的 where 元素定义 JPQL 条件

假设视图有两个 UI 组件用来输入条件参数:nameFilterField 文本控件和 hrManagerFilterField 复选框:

<textField id="nameFilterField" label="Name"/>
<entityComboBox id="hrManagerFilterField" label="HR Manager"
                metaClass="User" itemsContainer="usersDc">
    <actions>
        <action id="entityClear" type="entity_clear"/>
    </actions>
</entityComboBox>

为了在用户改变输入值的时候刷新数据,需要在视图控制器添加事件监听器:

@ViewComponent
private CollectionLoader<Department> departmentsDl;

@Subscribe("nameFilterField")
public void onNameFilterFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<TextField, String> event) {
    departmentsDl.setParameter("name", "(?i)%" + event.getValue() + "%");
    departmentsDl.load();
}

@Subscribe("hrManagerFilterField")
public void onHrManagerFilterFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<EntityComboBox<User>, User> event) {
    departmentsDl.setParameter("hrManager", event.getValue());
    departmentsDl.load();
}

如上面所说,只有在条件的参数都设置了之后才会将条件添加到查询语句中。所以在数据库会执行什么样的查询语句依赖于在 UI 组件如何输入参数:

只有 nameFilterField 有值
select e from Department e where e.name like :name
只有 hrManagerFilterField 有值
select e from Department e where e.hrManager = :hrManager
nameFilterField 和 hrManagerFilterField 都有值
select e from Department e where (e.name like :name) and (e.hrManager = :hrManager)