数据加载器

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

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

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

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

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

在界面的 XML 描述中,所有的加载器都用同一个 <loader> 元素中定义,加载器的类型通过包裹它的容器类型确定。

数据加载器不是必要的,因为可以使用 DataManager 或者自定义的服务来加载数据,然后直接设置给容器。但通过在界面中声明式的定义加载器可以简化数据加载的过程,特别是要使用 过滤器 组件的情况下。

通常,集合加载器从界面的描述文件中获得 JPQL 查询语句,然后从过滤器组件拿到查询参数,之后创建 LoadContext 并调用 DataManager 加载实体。所以,典型的 XML 描述看起来是这样:

<data readOnly="true">
    <collection id="customersDc"
                class="ui.ex1.entity.Customer">
        <fetchPlan extends="_base"/>
        <loader id="customersDl" >
            <query>
                <![CDATA[select e from uiex1_Customer e]]>
            </query>
        </loader>
    </collection>
</data>
<layout expand="customersTable" spacing="true">
    <filter id="filter"
            dataLoader="customersDl">
        <properties include=".*"/>
    </filter>
    <!-- ... -->
</layout>

在实体编辑界面,加载器的 XML 元素通常是空的,因为实例加载器需要一个实体的标识符,这个标识符通过编程的方式使用 StandardEditor 基类指定。

<data>
    <instance id="customerDc"
              class="ui.ex1.entity.Customer">
        <fetchPlan extends="_base"/>
        <loader/>
    </instance>
</data>

加载器也可以编程式创建和配置,示例:

@Autowired
private DataComponents dataComponents;

private CollectionLoader<Customer> customersDl;

private void createCustomerLoader(CollectionContainer<Customer> container) {
    customersDl = dataComponents.createCollectionLoader();
    customersDl.setQuery("select e from uiex1_Customer e");
    customersDl.setContainer(container);
    customersDl.setDataContext(getScreenData().getDataContext());
}

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

事件和处理器

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

如需使用 Jmix Studio 生成处理器的桩代码,在界面 XML 描述或者 Component Hierarchy(组件层级结构) 面板中选择数据容器元素,然后使用 Component Inspector(组件探查器) 面板的 Handlers 标签页。

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

loadDelegate

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

@Autowired
private CustomerService customerService;

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

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

如果使用代理自定义数据加载过程,并在表格中使用分页器组件(PaginationSimplePagination)显示加载的数据,那么还需要自定义逻辑来统计数据的总行数。请参阅与表格关联分页器组件的 TotalCountDelegate 处理器。

PreLoadEvent

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

@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
public void onCustomersDlPreLoad(CollectionLoader.PreLoadEvent<Customer> event) {
    // some actions before loading
}

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

PostLoadEvent

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

@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
public void onCustomersDlPostLoad(CollectionLoader.PostLoadEvent<Customer> event) {
    // some actions after loading
}

查询条件

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

不需要使用全局过滤器或者添加全局过滤器,而是可以为加载器查询语句单独创建一组过滤条件。一个过滤条件是一组带有参数的查询子句。子句中所有的参数都设置了之后,这些子句才会被添加到生成的查询语句文本中。过滤条件会在数据存储级别传递,因此可以包含各个数据存储支持的不同语言的子句。框架会提供 JPQL 的过滤条件。

下面例子中,按照 Person 实体的两个属性:string name 和 boolean status 对实体进行过滤。

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

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

假设界面有两个 UI 组件用来输入条件参数:nameFilterField 文本控件和 statusFilterField 复选框。为了在用户改变它们值的时候刷新数据,需要在界面控制器添加事件监听器:

@Autowired
private CollectionLoader<Person> personsDl;

@Subscribe("nameFilterField")
public void onNameFilterFieldValueChange1(HasValue.ValueChangeEvent event) {
    if (event.getValue() != null) {
        personsDl.setParameter("name", "(?i)%" + event.getValue() + "%");
    } else {
        personsDl.removeParameter("name");
    }
    personsDl.load();
}

@Subscribe("statusFilterField")
public void onStatusFilterFieldValueChange(HasValue.ValueChangeEvent<Boolean> event) {
    if (event.getValue()) {
        personsDl.setParameter("status", true);
    } else {
        personsDl.removeParameter("status");
    }
    personsDl.load();
}

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

只有 nameFilterField 有值
select e from uiex1_Person e where e.name like :name
只有 statusFilterField 有值
select e from uiex1_Person e where e.status = :status
nameFilterField 和 statusFilterField 都有值
select e from uiex1_Person e where (e.name like :name) and (e.status = :status)