3.5.3.2. 数据加载器

数据加载器用来从中间层加载数据到数据容器

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

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

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

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

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

数据加载器不是必选的,因为可以使用 DataManager 或者自定义的服务来加载数据,之后直接设置给容器。但是使用加载器通过在界面中声明式的定义可以简化数据加载的过程,特别是要使用过滤器组件的情况下。通常,集合加载器从界面的描述文件中获得 JPQL 查询语句,然后从过滤器组件拿到查询参数,之后创建 LoadContext 并且调用 DataManager 加载实体。所以,典型的 XML 描述看起来是这样:

<data>
    <collection id="customersDc" class="com.company.sample.entity.Customer" view="_local">
        <loader id="customersDl">
            <query>
                select e from sample_Customer e
            </query>
        </loader>
    </collection>
</data>
<layout>
    <filter id="filter" applyTo="customersTable" dataLoader="customersDl">
        <properties include=".*"/>
    </filter>
    <!-- ... -->
</layout>

loader XML 元素的属性可以用来定义可选参数,比如 cacheablesoftDeletion 等。

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

<data>
    <instance id="customerDc" class="com.company.sample.entity.Customer" view="_local">
        <loader/>
    </instance>
</data>

加载器可以将实际的加载动作代理到一个函数,这个函数可以通过 setLoadDelegate() 方法或者通过在界面控制器中使用 @Install 注解来声明式的提供。示例:

@Inject
private DataManager dataManager;

@Install(to = "customersDl", target = Target.DATA_LOADER)
protected List<Customer> customersDlLoadDelegate(LoadContext<Customer> loadContext) {
    return dataManager.loadList(loadContext);
}

在上面的例子中,customersDl 加载器会使用 customersDlLoadDelegate() 方法来加载 Customer 实体列表。此方法接收 LoadContext 参数,加载器会按照它的参数(查询语句、过滤器等等)来创建这个参数。在这个例子中,数据加载是通过 DataManager 来完成的,这个实现跟标准加载器的实现一样高效,但是好处是可以使用自定义的服务或者可以在加载完实体之后做其它的事情。

一个加载器也可以通过编程的方式创建和配置,示例:

@Inject
private DataComponents dataComponents;

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

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

查询条件

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

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

作为例子,考虑按照 Customer 实体的两个属性:string name 和 boolean status 对实体进行过滤,看看如何创建一组过滤条件。

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

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd" (1)
        caption="Customers browser" focusComponent="customersTable">
    <data>
        <collection id="customersDc"
                    class="com.company.demo.entity.Customer" view="_local">
            <loader id="customersDl">
                <query><![CDATA[select e from demo_Customer 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 条件命名空间
2 - 在 query 内定义 condition 元素
3 - 如果有多个条件,添加 andor 元素
4 - 使用可选的 join 元素和必须的 where 元素定义 JPQL 条件

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

@Inject
private CollectionLoader<Customer> customersDl;

@Subscribe("nameFilterField")
private void onNameFilterFieldValueChange(HasValue.ValueChangeEvent<String> event) {
    if (event.getValue() != null) {
        customersDl.setParameter("name", "(?i)%" + event.getValue() + "%"); (1)
    } else {
        customersDl.removeParameter("name");
    }
    customersDl.load();
}

@Subscribe("statusFilterField")
private void onStatusFilterFieldValueChange(HasValue.ValueChangeEvent<Boolean> event) {
    if (event.getValue()) {
        customersDl.setParameter("status", true);
    } else {
        customersDl.removeParameter("status");
    }
    customersDl.load();
}
1 - 注意这里怎么使用 ORM 提供的不区分大小写的子串搜索

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

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