Использование компонентов данных

В данном разделе рассмотрены практические примеры работы с компонентами данных.

Декларативное использование

Обычно компоненты данных определяются и привязываются к визуальным компонентам декларативно в XML-дескрипторе экрана. При создании экрана для сущности с помощью Studio, можно увидеть корневой элемент <data>, содержащий объявления компонентов данных.

Ниже приведен пример компонентов данных на экране редактирования для сущности Employee, который содержит to-one ссылку на Department и to-many ссылку на сущность EquipmentLine:

<data> (1)
    <instance id="employeeDc"
              class="ui.ex1.entity.Employee"> (2)
        <fetchPlan extends="_base"> (3)
            <property name="department" fetchPlan="_instance_name"/>
            <property name="equipment" fetchPlan="_base"/>
        </fetchPlan>
        <loader/>(4)
        <collection id="equipmentDc" property="equipment"/> (5)
    </instance>
    <collection id="departmentsDc"
                class="ui.ex1.entity.Department"
                fetchPlan="_base"> (6)
        <loader> (7)
            <query>
                <![CDATA[select e from uiex1_Department e]]>
            </query>
        </loader>
    </collection>
</data>
1 Корневой элемент data определяет экземпляр DataContext.
2 Контейнер InstanceContainer сущности Employee.
3 Опциональный атрибут fetchPlan определяет граф объектов, который должен быть жадно загружен из базы данных.
4 InstanceLoader, загружающий экземпляры сущности Employee.
5 Контейнер CollectionPropertyContainer для вложенной сущности EquipmentLine. Этот контейнер привязан к атрибуту-коллекции Employee.equipment.
6 Контейнер CollectionContainer для сущности Department.
7 CollectionLoader, загружающий экземпляры сущности Department по определенному запросу.

Заданные выше контейнеры данных могут использоваться в визуальных компонентах следующим образом:

<layout spacing="true" expand="editActions">
    <textField dataContainer="employeeDc" property="id"/> (1)
    <form id="form" dataContainer="employeeDc"> (2)
        <column width="350px">
            <textField id="nameField" property="name"/>
            <textField id="salaryField" property="salary"/>
            <comboBox id="positionField" property="position"/>
            <entityComboBox property="department"
                            optionsContainer="departmentsDc"/> (3)
        </column>
    </form>
    <table dataContainer="equipmentDc"> (4)
        <columns>
            <column id="name"/>
            <column id="number"/>
        </columns>
    </table>
</layout>
1 Отдельные поля имеют атрибуты dataContainer и property.
2 Элемент form распространяет свой dataContainer на все вложенные поля, поэтому они требуют только указания атрибута property.
3 Поле EntityComboBox имеет атрибут optionsContainer для получения списка опций.
4 У таблиц есть только атрибут dataContainer.

Программное использование

Компоненты данных можно создавать и использовать программно.

В следующем примере мы создадим экран редактирования с тем же данными и визуальными компонентами, которые мы определяли декларативно в предыдущем примере, на чистой Java без XML-дескриптора.

@UiController("uiex1_EmployeeExample.edit")
public class EmployeeEditExample extends StandardEditor<Employee> {

    @Autowired
    private DataComponents dataComponents; (1)
    @Autowired
    private UiComponents uiComponents;

    private InstanceContainer<Employee> employeeDc;
    private CollectionPropertyContainer<EquipmentLine> equipmentDc;
    private CollectionContainer<Department> departmentsDc;
    private InstanceLoader<Employee> employeeDl;
    private CollectionLoader<Department> departmentsDl;

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

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

        employeeDc = dataComponents.createInstanceContainer(Employee.class);

        employeeDl = dataComponents.createInstanceLoader();
        employeeDl.setContainer(employeeDc); (3)
        employeeDl.setDataContext(dataContext); (4)
        employeeDl.setFetchPlan(FetchPlan.BASE);

        equipmentDc = dataComponents.createCollectionContainer(
                EquipmentLine.class, employeeDc, "equipment"); (5)

        departmentsDc = dataComponents.createCollectionContainer(Department.class);

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

    private void createUiComponents() {

        Form form = uiComponents.create(Form.class);
        getWindow().add(form);

        TextField<String> nameField = uiComponents.create(TextField.TYPE_STRING);
        nameField.setValueSource(new ContainerValueSource<>(employeeDc, "name")); (7)
        form.add(nameField);

        TextField<Double> salaryField = uiComponents.create(TextField.TYPE_DOUBLE);
        salaryField.setValueSource(new ContainerValueSource<>(employeeDc, "salary"));
        form.add(salaryField);

        ComboBox<Position> positionField = uiComponents.create(ComboBox.of(Position.class));
        positionField.setValueSource(new ContainerValueSource<>(employeeDc, "position"));
        form.add(positionField);

        EntityComboBox<Department> departmentField = uiComponents.create(EntityComboBox.of(Department.class));
        departmentField.setValueSource(new ContainerValueSource<>(employeeDc, "department"));
        departmentField.setOptions(new ContainerOptions<>(departmentsDc)); (8)
        form.add(departmentField);

        Table<EquipmentLine> table = uiComponents.create(Table.of(EquipmentLine.class));
        getWindow().add(table);
        getWindow().expand(table);
        table.setItems(new ContainerTableItems<>(equipmentDc)); (9)

        Button okButton = uiComponents.create(Button.class);
        okButton.setCaption("OK");
        okButton.addClickListener(clickEvent -> closeWithCommit());
        getWindow().add(okButton);

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

    @Override
    protected InstanceContainer<Employee> getEditedEntityContainer() { (10)
        return employeeDc;
    }

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) { (11)
        employeeDl.load();
        departmentsDl.load();
    }
}
1 DataComponents - это фабрика для создания компонентов данных.
2 Регистрируем в экране экземпляр DataContext, чтобы обеспечить работу стандартного действия commit.
3 Загрузчик employeeDl загружает данные в контейнер employeeDc.
4 Загрузчик employeeDl помещает загруженные сущности в data context для отслеживания изменений.
5 equipmentDc создается как контейнер свойства.
6 Определяем запрос для загрузчика departmentsDl.
7 ContainerValueSource используется для связи одиночных полей с контейнерами данных.
8 ContainerOptions предоставляет список опций для полей выбора.
9 ContainerTableItems используется для связи таблиц с контейнерами.
10 Переопределяем getEditedEntityContainer(), чтобы указать контейнер, вместо аннотации @EditedEntityContainer.
11 Загружаем данные перед отображением экрана. Идентификатор редактируемой сущности будет автоматически передан в загрузчик employeeDl.

Зависимости между компонентами данных

Иногда требуется загружать и отображать данные, которые зависят от других данных в том же экране. К примеру, на скриншоте ниже таблица слева отображает список сотрудников, а таблица справа – список оборудования для выбранного сотрудника. Список справа обновляется каждый раз, когда меняется выбранный элемент в таблице слева.

depend tables

В этом примере сущность Employee содержит атрибут equipment, который является коллекцией с отношением one-to-many. Самый простой способ реализации экрана – загружать список заказов с фетч-планом, содержащим атрибут equipment, и использовать контейнер свойств для работы со списком зависимых строк. Затем мы связываем левую таблицу с родительским контейнером, а правую – с контейнером свойства.

Однако этот подход может иметь последствия для производительности, ведь мы загружаем все строки для всех сотрудников из левой таблицы, несмотря на то, что в один момент времени отображаются строки только для одного выбранного сотрудника. Поэтому мы рекомендуем использовать контейнеры свойств и расширенные фетч-планы только тогда, когда нужно загрузить единственный экземпляр родительской сущности: например, в экране редактирования одного сотрудника.

Кроме того, родительская сущность может не иметь прямого атрибута, указывающего на зависимую сущность. В этом случае подход с использованием контейнера свойств совсем не подходит.

Наилучшей практикой организации отношений между данными в экране является использование запросов с параметрами. Зависимый загрузчик содержит запрос с параметром, который связывает данные с родительским контейнером, и когда меняется текущий экземпляр в родительском контейнере, мы передаем его в качестве параметра и вызываем зависимый загрузчик.

Рассмотрим пример экрана, в котором есть две зависимых пары контейнер/загрузчик и привязанные к ним таблицы для отображения данных.

<window xmlns="http://jmix.io/schema/ui/window"
        xmlns:c="http://jmix.io/schema/ui/jpql-condition"
        caption="msg://employeeDependTables.caption"
        focusComponent="employeesTable">
    <data>
        <collection id="employeesDc"
                    class="ui.ex1.entity.Employee"
                    fetchPlan="_base"> (1)
            <loader id="employeesDl">
                <query>
                    <![CDATA[select e from uiex1_Employee e]]>
                </query>
            </loader>
        </collection>
        <collection id="equipmentLinesDc"
                    class="ui.ex1.entity.EquipmentLine"
                    fetchPlan="_base"> (2)
            <loader id="equipmentLinesDl">
                <query>
                    <![CDATA[select e from uiex1_EquipmentLine e where e.employee = :employee]]>
                </query>
            </loader>
        </collection>
    </data>
    <facets> (3)
        <screenSettings id="settingsFacet" auto="true"/>
    </facets>
    <layout>
        <hbox id="mainBox" width="100%" height="100%" spacing="true">
            <table id="employeesTable" width="100%" height="100%"
                   dataContainer="employeesDc"> (4)
                <columns>
                    <column id="name"/>
                    <column id="salary"/>
                    <column id="position"/>
                </columns>
            </table>
            <table id="equipmentLinesTable" width="100%" height="100%"
                   dataContainer="equipmentLinesDc"> (5)
                <columns>
                    <column id="name"/>
                    <column id="number"/>
                </columns>
            </table>
        </hbox>
    </layout>
</window>
1 Родительский контейнер и загрузчик.
2 Дочерний контейнер и загрузчик.
3 Фасет DataLoadCoordinator не используется, поэтому загрузчики не будут вызваны автоматически.
4 Основная таблица.
5 Зависимая таблица.
@UiController("uiex1_EmployeeDependTables")
@UiDescriptor("employee-depend-tables.xml")
@LookupComponent("employeesTable")
public class EmployeeDependTables extends StandardLookup<Employee> {

    @Autowired
    private CollectionLoader<Employee> employeesDl;

    @Autowired
    private CollectionLoader<EquipmentLine> equipmentLinesDl;

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

    @Subscribe(id = "employeesDc", target = Target.DATA_CONTAINER)
    public void onEmployeesDcItemChange(InstanceContainer.ItemChangeEvent<Employee> event) {
        equipmentLinesDl.setParameter("employee", event.getItem()); (2)
        equipmentLinesDl.load();
    }
}
1 Родительский загрузчик вызывается обработчиком BeforeShowEvent.
2 В обработчике родительского контейнера ItemChangeEvent передаем параметр в зависимый загрузчик и вызываем его.
Фасет DataLoadCoordinator позволяет устанавливать связи между компонентами данных декларативно без написания кода на Java.

Использование параметров экрана в загрузчиках

Часто бывает необходимо загружать данные в экране в зависимости от параметров, переданных в этот экран. В данном разделе приведен пример экрана, принимающего параметр и использующего его для фильтрации загружаемых данных.

Предположим, имеются две сущности: Country и City. У сущности City есть атрибут country, который является ссылкой на Country. Экран со списком городов принимает экземпляр страны и отображает города только этой страны.

Рассмотрим XML-дескриптор экрана со списком городов. Его загрузчик содержит запрос с параметром:

<data>
    <collection id="citiesDc"
                class="ui.ex1.entity.City"
                fetchPlan="_base">
        <loader id="citiesDl">
            <query>
                <![CDATA[select e from uiex1_City e where e.country = :country]]>
            </query>
        </loader>
    </collection>
</data>

Контроллер экрана городов содержит публичный метод-setter для параметра и использует этот параметр в обработчике BeforeShowEvent.

@UiController("sample_CityBrowse")
@UiDescriptor("city-browse.xml")
@LookupComponent("citiesTable")
public class CityBrowse extends StandardLookup<City> {

    @Autowired
    private CollectionLoader<City> citiesDl;

    private Country country;

    public void setCountry(Country country) {
        this.country = country;
    }

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        if (country == null)
            throw new IllegalStateException("Country parameter is null");
        citiesDl.setParameter("country", country);
        citiesDl.load();
    }
}

Экран городов можно вызвать из другого экрана, передавая параметр, как показано ниже:

@Autowired
private ScreenBuilders screenBuilders;

private void showCitiesOfCountry(Country country) {
    CityBrowse cityBrowse = screenBuilders.screen(this)
            .withScreenClass(CityBrowse.class)
            .build();
    cityBrowse.setCountry(country);
    cityBrowse.show();
}

Специализированная сортировка

Сортировка таблиц по атрибутам сущности в UI производится объектом типа CollectionContainerSorter, который устанавливается для CollectionContainer. Стандартная реализация сортирует данные в памяти, если загруженный список умещается на одну страницу, или посылает запрос с соответствующим "order by" в базу данных. Выражение "order by" формируется бином JpqlSortExpressionProvider.

Некоторые атрибуты могут потребовать специальной реализации сортировки. Ниже рассматривается простой пример: предположим, в сущности Order есть атрибут number типа String, но на самом деле атрибут хранит только числовые значения. Поэтому необходимо иметь порядок сортировки для чисел: 1, 2, 3, 10, 11. Стандартный механизм сортировки в данном случае выдаст порядок 1, 10, 11, 2, 3.

Сначала создайте наследника класса CollectionContainerSorter для сортировки в памяти:

public class CustomCollectionContainerSorter extends CollectionContainerSorter {

    public CustomCollectionContainerSorter(CollectionContainer container,
                                           @Nullable BaseCollectionLoader loader,
                                           BeanFactory beanFactory) {
        super(container, loader, beanFactory);
    }

    @Override
    protected Comparator<?> createComparator(Sort sort, MetaClass metaClass) {
        MetaPropertyPath metaPropertyPath = Objects.requireNonNull(
                metaClass.getPropertyPath(sort.getOrders().get(0).getProperty()));

        if (metaPropertyPath.getMetaClass().getJavaClass().equals(Order.class)
                && "num".equals(metaPropertyPath.toPathString())) {
            boolean isAsc = sort.getOrders().get(0).getDirection() == Sort.Direction.ASC;
            return Comparator.comparing((Order e) ->
                            e.getNum() == null ? null : Integer.valueOf(e.getNum()),
                    new EntityValuesComparator<>(isAsc, metaClass, beanFactory));
        }
        return super.createComparator(sort, metaClass);
    }
}

Создайте сортировщик в нужном экране:

public class OrderBrowseExample extends StandardLookup<Order> {

    @Autowired
    private CollectionLoader<Order> ordersDl;

    @Autowired
    private CollectionContainer<Order> ordersDc;

    @Autowired
    private BeanFactory beanFactory;

    @Subscribe
    private void onInit(InitEvent event) {
        Sorter sorter = new CustomCollectionContainerSorter(ordersDc, ordersDl, beanFactory);
        ordersDc.setSorter(sorter);
    }
}

Если же специализированная сортировка должна являться глобальной, то создайте собственную фабрику, которая будет инстанциировать сортировщик для всей системы:

@Primary
@Component("sample_SorterFactory")
public class CustomSorterFactory extends SorterFactory {

    @Override
    public Sorter createCollectionContainerSorter(CollectionContainer container,
                                                  @Nullable BaseCollectionLoader loader) {
        return new CustomCollectionContainerSorter(container, loader, beanFactory);
    }
}

Также можно создать собственную реализацию JpqlSortExpressionProvider для сортировки на уровне базы данных:

@Primary
@Component("sample_JpqlSortExpressionProvider")
public class CustomSortExpressionProvider
        extends DefaultJpqlSortExpressionProvider {

    @Override
    public String getDatatypeSortExpression(MetaPropertyPath metaPropertyPath, boolean sortDirectionAsc) {
        if (metaPropertyPath.getMetaClass().getJavaClass().equals(Order.class)
                && "num".equals(metaPropertyPath.toPathString())) {
            return String.format("CAST({E}.%s BIGINT)", metaPropertyPath);
        }
        return String.format("{E}.%s", metaPropertyPath);
    }

}