使用数据组件

在本章节将展示使用数据组件的实战例子。

声明式用法

通常,数据组件都是在视图 XML 描述中声明式地定义并绑定至可视化组件。如果使用 Studio 为实体创建视图,可以看到 XML 中的 <data> 元素,包含数据组件的声明。

下面是 User 实体详情视图中的数据组件示例,UserDepartment 实体有 to-one 的关联,和 UserStep 实体有 to-many 关联:

<data> (1)
    <instance id="userDc"
              class="com.company.onboarding.entity.User"> (2)
        <fetchPlan extends="_base"> (3)
            <property name="department" fetchPlan="_base"/>
            <property name="steps" fetchPlan="_base">
                <property name="step" fetchPlan="_base"/>
            </property>
        </fetchPlan>
        <loader/> (4)
        <collection id="stepsDc" property="steps"/> (5)
    </instance>
    <collection id="departmentsDc" class="com.company.onboarding.entity.Department"> (6)
        <fetchPlan extends="_base"/>
        <loader> (7)
            <query>
                <![CDATA[select e from Department e
                order by e.name asc]]>
            </query>
        </loader>
    </collection>
</data>
1 data 根元素定义 DataContext 实例。
2 User 实体的 InstanceContainer
3 可选的 fetchPlan 属性,定义从数据库预加载的对象关系图。
4 InstanceLoader,加载 User 实例。
5 内部 UserStep 实体的 CollectionPropertyContainer。绑定至 User.steps 集合属性。
6 Department 实体的 CollectionContainer。可以用于选择部门的下拉框选项列表。
7 CollectionLoader 用指定的查询语句加载 Department 实体实例。

上面数据容器可以在可视化组件中这样使用:

<textField id="usernameField" dataContainer="userDc" property="username"/> (1)
<formLayout id="form" dataContainer="userDc"> (2)
    <textField id="firstNameField" property="firstName"/>
    <textField id="lastNameField" property="lastName"/>
    <entityComboBox id="departmentField" property="department"
                    itemsContainer="departmentsDc"/> (3)
</formLayout>
<dataGrid id="stepsDataGrid" width="100%" minHeight="10em"
          dataContainer="stepsDc"> (4)
    <columns>
        <column property="step"/>
        <column property="dueDate"/>
        <column property="completedDate"/>
    </columns>
</dataGrid>
1 单独的控件具有 dataContainerproperty 属性。
2 formLayout 组件会将 dataContainer 传递给表单内部的字段,所以字段只需要 property 属性。
3 entityComboBox 也有 itemsContainer 属性,获取选项列表。
4 dataGrid 只有 dataContainer 属性。

编程式用法

可以使用编程的方式在可视化组件中创建和使用数据组件。

下面的例子中,我们创建了跟上一节一样的详情视图,使用了相同的数据和可视化组件,只不过是用纯 Java 实现的。

@Route(value = "users2/:id", layout = MainView.class)
@ViewController("User.detail2")
public class UserDetailViewProgrammatic extends StandardDetailView<User> {

    @Autowired
    private DataComponents dataComponents; (1)
    @Autowired
    private UiComponents uiComponents;
    @Autowired
    private FetchPlans fetchPlans;
    @Autowired
    private Metadata metadata;

    private InstanceContainer<User> userDc;
    private InstanceLoader<User> userDl;
    private CollectionPropertyContainer<UserStep> stepsDc;
    private CollectionContainer<Department> departmentsDc;
    private CollectionLoader<Department> departmentsDl;

    @Subscribe
    public void onInit(InitEvent event) {
        createDataComponents();
        createUiComponents();
    }

    private void createDataComponents() {
        DataContext dataContext = dataComponents.createDataContext();
        getViewData().setDataContext(dataContext); (2)

        userDc = dataComponents.createInstanceContainer(User.class);

        userDl = dataComponents.createInstanceLoader();
        userDl.setContainer(userDc); (3)
        userDl.setDataContext(dataContext); (4)

        FetchPlan userFetchPlan = fetchPlans.builder(User.class)
                .addFetchPlan(FetchPlan.BASE)
                .add("department", FetchPlan.BASE)
                .add("steps", FetchPlan.BASE)
                .add("steps.step", FetchPlan.BASE)
                .build();
        userDl.setFetchPlan(userFetchPlan);

        stepsDc = dataComponents.createCollectionContainer(
                UserStep.class, userDc, "steps"); (5)

        departmentsDc = dataComponents.createCollectionContainer(Department.class);

        departmentsDl = dataComponents.createCollectionLoader();
        departmentsDl.setContainer(departmentsDc);
        departmentsDl.setDataContext(dataContext);
        departmentsDl.setQuery("select e from Department e"); (6)
        departmentsDl.setFetchPlan(FetchPlan.BASE);
    }

    private void createUiComponents() {
        TypedTextField<String> usernameField = uiComponents.create(TypedTextField.class);
        usernameField.setValueSource(new ContainerValueSource<>(userDc, "username")); (7)
        getContent().add(usernameField);

        FormLayout formLayout = uiComponents.create(FormLayout.class);
        getContent().add(formLayout);

        TypedTextField<String> firstNameField = uiComponents.create(TypedTextField.class);
        firstNameField.setValueSource(new ContainerValueSource<>(userDc, "firstName"));
        formLayout.add(firstNameField);

        TypedTextField<String> lastNameField = uiComponents.create(TypedTextField.class);
        lastNameField.setValueSource(new ContainerValueSource<>(userDc, "lastName"));
        formLayout.add(lastNameField);

        EntityComboBox<Department> departmentField = uiComponents.create(EntityComboBox.class);
        departmentField.setValueSource(new ContainerValueSource<>(userDc, "department"));
        departmentField.setItems(departmentsDc); (8)
        formLayout.add(departmentField);

        DataGrid<UserStep> dataGrid = uiComponents.create(DataGrid.class);
        dataGrid.addColumn(metadata.getClass(UserStep.class).getPropertyPath("step.name"));
        dataGrid.setItems(new ContainerDataGridItems<>(stepsDc)); (9)
        getContent().add(dataGrid);
        getContent().expand(dataGrid);

        Button okButton = uiComponents.create(Button.class);
        okButton.setText("OK");
        okButton.addClickListener(clickEvent -> closeWithSave());
        getContent().add(okButton);

        Button cancelButton = uiComponents.create(Button.class);
        cancelButton.setText("Cancel");
        cancelButton.addClickListener(clickEvent -> closeWithDiscard());
        getContent().add(cancelButton);
    }

    @Override
    protected InstanceContainer<User> getEditedEntityContainer() { (10)
        return userDc;
    }

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) { (11)
        userDl.load();
        departmentsDl.load();
    }
}
1 DataComponents 是创建数据组件的工厂。
2 DataContext 实例在视图注册,以便能使用标准的提交操作。
3 userDl 加载器会加载数据到 userDc 容器。
4 userDl 加载器会合并加载的实体到数据上下文以便跟踪改动。
5 stepsDc 创建为属性容器。
6 departmentsDl 加载器指定了一个查询语句。
7 ContainerValueSource 用来绑定单一字段和容器。
8 CollectionContainer 用来为查找控件提供选项。
9 ContainerDataGridItems 用来绑定表格到容器。
10 getEditedEntityContainer() 被重写了,用来指定容器,替代了 @EditedEntityContainer 注解。
11 在视图展示前加载数据。框架会自动设置编辑实体的 id 到 userDl

数据组件依赖

有时候需要加载和展示依赖同一视图上其他数据的数据。例如,在下面的图片中,左边的表格展示用户列表(User 实体),右边的表格展示选中用户的入职步骤(UserStep 实体)。右边的列表会在左边列表每次变更选择时刷新。

dependent tables

这个例子中,User 实体包含了 steps 属性,这个是一对多的集合。所以实现这个视图的最简单的方法就是使用带有 steps 属性的 fetch plan 加载用户列表,并且使用 属性容器 来保存 UserStep 列表。然后绑定左边的表格到主容器,绑定右边的表格到属性容器。

但是这个方案有一个隐藏的性能问题:会加载左边表格所有用户的所有 UserStep 实体,尽管每次只是展示单一用户的 UserStep。这就是为什么我们推荐只在加载单一主实体的时候使用属性容器和深度 fetch plan 处理集合属性,例如,在用户的详情视图中。

还有,主实体也许跟依赖的实体没有直接的属性关联关系。这种情况下,上面使用属性容器的方案根本就行不通。

组织视图内数据关系的通常方法是使用带参数的查询。依赖的加载器包含一个带参数的查询语句,这个参数关联到主实体的数据,当主容器的当前实体更改时,需要手动设置参数并且触发依赖的加载器。

下面这个例子的视图包含两对相互依赖的容器/加载器以及绑定的表格。

<view xmlns="http://jmix.io/schema/flowui/view"
      title="Users with onboarding steps"
      focusComponent="usersTable">
    <data readOnly="true">
        <collection id="usersDc"
                    class="com.company.onboarding.entity.User"> (1)
            <fetchPlan extends="_base"/>
            <loader id="usersDl">
                <query>
                    <![CDATA[select e from User e order by e.username asc]]>
                </query>
            </loader>
        </collection>
        <collection id="userStepsDc"
                    class="com.company.onboarding.entity.UserStep"> (2)
            <fetchPlan extends="_base"/>
            <loader id="userStepsDl">
                <query>
                    <![CDATA[select e from UserStep e where e.user = :user
                    order by e.sortValue asc]]>
                </query>
            </loader>
        </collection>
    </data>
    <facets/> (3)
    <layout>
        <formLayout>
            <dataGrid id="usersTable"
                      dataContainer="usersDc"> (4)
                <columns>
                    <column property="username"/>
                    <column property="firstName"/>
                    <column property="lastName"/>
                </columns>
            </dataGrid>
            <dataGrid id="userStepsTable"
                      dataContainer="userStepsDc"> (5)
                <columns>
                    <column property="step.name"/>
                    <column property="dueDate"/>
                    <column property="completedDate"/>
                </columns>
            </dataGrid>
        </formLayout>
    </layout>
</view>
1 主容器和主加载器。
2 依赖容器和加载器。
3 由于没有使用 DataLoadCoordinator facet,我们会在控制器中以编程的方式触发加载器。
4 主表格。
5 从表格。
@Route(value = "users-with-steps", layout = MainView.class)
@ViewController("UserWithStepsListView")
@ViewDescriptor("user-with-steps-list-view.xml")
@LookupComponent("usersTable")
@DialogMode(width = "50em", height = "37.5em")
public class UserWithStepsListView extends StandardListView<User> {

    @ViewComponent
    private CollectionLoader<User> usersDl;
    @ViewComponent
    private CollectionLoader<UserStep> userStepsDl;

    @Subscribe
    public void onBeforeShow(final BeforeShowEvent event) {
        usersDl.load(); (1)
    }

    @Subscribe(id = "usersDc", target = Target.DATA_CONTAINER)
    public void onUsersDcItemChange(final InstanceContainer.ItemChangeEvent<User> event) {
        userStepsDl.setParameter("user", event.getItem()); (2)
        userStepsDl.load();
    }
}
1 主数据加载器在 BeforeShowEvent 处理器中触发。
2 在主数据容器的 ItemChangeEvent 处理器中,为依赖的加载器设置参数并触发加载。
使用 DataLoadCoordinator facet 可以将数据组件通过声明式的方式连接,不需要写 Java 代码。

排序

与集合绑定的 UI 组件(例如 dataGrid)使用底层 CollectionContainer 定义的排序顺序。Jmix 对内存中的排序和基于 JPQL 的排序都使用默认的行为,并提供了自定义排序的扩展点。

默认行为

排序的工作方式如下:

  1. UI 组件调用与集合容器关联的 Sorter

  2. 框架通过 CollectionContainerSortManager 获取该排序器。

  3. 如果没有 provider 返回排序器,则使用 SorterFactory 创建标准的 CollectionContainerSorterCollectionPropertyContainerSorter

当数据容器没有加载器时,或者仅加载了第一页且加载的项目数小于页面大小时,标准排序器会在内存中对数据进行排序。否则,会按照需要的排序规则从数据库重新加载数据。有关 JPQL 排序的详细信息,请参见 自定义 JPQL 排序表达式

自定义内存排序

某些实体属性可能需要自定义排序逻辑。例如,Department 实体可能有一个类型为 Stringnum 属性,但存储的值表示数字。将其作为字符串排序会得到 1101123,而按数字排序则需要给出更自然的顺序:1231011

如需为特定实体自定义内存排序:

  1. 创建 CollectionContainerSorter 的子类(或者提供完全自定义的 Sorter 实现):

    public class CustomCollectionContainerSorter extends CollectionContainerSorter {
    
        public CustomCollectionContainerSorter(CollectionContainer<?> container,
                                               BaseCollectionLoader loader,
                                               BeanFactory beanFactory) {
            super(container, loader, beanFactory);
        }
    
        @Override
        protected Comparator<?> createComparator(Sort.Order sortOrder, MetaClass metaClass) {
            MetaPropertyPath metaPropertyPath = Objects.requireNonNull(
                    metaClass.getPropertyPath(sortOrder.getProperty()));
    
            if (metaPropertyPath.getMetaClass().getJavaClass().equals(Department.class)
                    && "num".equals(metaPropertyPath.toPathString())) {
                boolean isAsc = sortOrder.getDirection() == Sort.Direction.ASC;
                return Comparator.comparing((Department e) ->
                                e.getNum() == null ? null : Integer.valueOf(e.getNum()),
                        new EntityValuesComparator<>(isAsc, metaClass, beanFactory));
            }
            return super.createComparator(sortOrder, metaClass);
        }
    }
  2. 创建一个实现 CollectionContainerSortProvider 的 bean,并仅为支持的容器返回自定义排序器:

    @Component
    @Order(100)
    public class CustomCollectionContainerSortProvider implements CollectionContainerSortProvider {
    
        @Autowired
        private BeanFactory beanFactory;
    
        @Nullable
        @Override
        public Sorter getSorter(CollectionContainerSortContext context) {
            if (supports(context)) {
                return new CustomCollectionContainerSorter(context.container(), context.loader(), beanFactory);
            }
    
            return null;
        }
    
        private boolean supports(CollectionContainerSortContext context) {
            return context.container().getEntityMetaClass().getJavaClass().equals(Department.class);
        }
    }
  3. 将容器与表格组件(如 DataGrid)绑定。当用户对表格排序时,框架使用为当前容器返回的排序器。

此方法自定义的是内存排序。如果数据容器有配置加载器,且排序应在从数据库重新加载数据时生效,还需要配置 JPQL 排序表达式

自定义 JPQL 排序表达式

当数据容器有配置相应的数据加载器时,排序会被转换为 JPQL。如需自定义生成的 JPQL 排序表达式,请注册一个实现 JpqlSortExpressionSupplier 的 bean。

如果 supplier 支持该属性,则返回 JPQL 排序表达式,否则返回 null 让下一个 supplier 处理:

@Component
@Order(100)
public class CustomSortExpressionSupplier implements JpqlSortExpressionSupplier {

    @Override
    @Nullable
    public String getDatatypeSortExpression(SortExpressionContext context) {
        if (context.metaPropertyPath().getMetaClass().getJavaClass().equals(Department.class)
                && "num".equals(context.metaPropertyPath().toPathString())) {
            return String.format("CAST({E}.%s BIGINT)", context.metaPropertyPath());
        }
        return null;
    }
}

Supplier 可以实现以下方法:

  • getDatatypeSortExpression(SortExpressionContext context)

  • getLobSortExpression(SortExpressionContext context)

ExpressionOrder

当排序无法用实体属性路径表示,而需要基于 JPQL 表达式时,可以使用 Sort.ExpressionOrder

与常规的 Sort.Order(按实体属性排序,例如 namecustomer.name)不同,ExpressionOrder 直接在 ORDER BY 中使用给定的 JPQL 表达式。可用于需要依赖函数、类型转换或其他自定义表达式排序时。

例如,JPQL 表达式可以是:

  • function('calc_total_sum', {E}.id)

  • length({E}.firstName)

  • {E}.firstName

以下示例按 name 属性的长度对部门进行排序:

<data readOnly="true">
    <collection id="departmentsDc"
                class="com.company.onboarding.entity.Department">
        <fetchPlan extends="_base"/>
        <loader id="departmentsDl">
            <query>
                <![CDATA[select e from Department e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <dataGrid id="departmentsDataGrid"
              width="100%"
              dataContainer="departmentsDc">
        <columns>
            <column property="name"/>
            <column property="num"/>
        </columns>
    </dataGrid>
</layout>
@ViewComponent
private CollectionLoader<Department> departmentsDl;

@Subscribe
public void onBeforeShow(final BeforeShowEvent event) {
    departmentsDl.setSort(Sort.by(
            Sort.ExpressionOrder.asc("length({E}.name)"), (1)
            Sort.Order.asc("name")
    ));
    departmentsDl.load(); (2)
}
1 Sort.ExpressionOrder.asc("length({E}.name)") 直接将 JPQL 表达式添加到排序中,并使用查询中根实体的 {E} 别名。
2 配置好排序后,显式执行加载器。

ExpressionOrder 不解析 MetaPropertyPath,会绕过 JpqlSortExpressionSupplier 和框架默认的 JpqlSortExpressionProvider。因此,需要开发者提供有效的 JPQL 排序表达式。

当查询返回 KeyValueEntity 时,ExpressionOrder 中不支持 {E} 别名。请使用与查询中相同的别名。例如,对于 select e.name, e.startTime, e.endTime from CarService e,使用 length(e.name)

多个排序 provider

Jmix 支持内存排序和 JPQL 排序表达式的多个排序 provider,并按 Spring 顺序解析:

  • 对于内存排序,可以注册多个实现 CollectionContainerSortProvider 的 bean。如果没有 provider 返回排序器,则回退到框架默认的 SorterFactory

  • 对于 JPQL 排序表达式,可以注册多个实现 JpqlSortExpressionSupplier 的 bean。如果没有 supplier 支持该属性,则回退到框架默认的 JpqlSortExpressionProvider

例如,扩展组件可以提供一个 provider,应用程序可以 提供另一个:

@Component
@Order(100)
public class AddonSortProvider implements CollectionContainerSortProvider {
    ...
}

@Component
@Order(200)
public class AppSortProvider implements CollectionContainerSortProvider {
    ...
}

此示例中,AddonSortProvider 会先使用,因为 order 值更小。

低优先级的 bean 可以作为应用程序范围的回退方案,同时也支持使用更具体的 bean 实现进行覆盖。