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

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

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

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

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

<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 Контейнер InstanceContainer сущности User.
3 Опциональный атрибут fetchPlan определяет граф объектов, который должен быть жадно загружен из базы данных.
4 InstanceLoader, загружающий экземпляры сущности User.
5 Контейнер CollectionPropertyContainer для вложенной сущности UserStep. Этот контейнер привязан к атрибуту-коллекции User.steps.
6 Контейнер CollectionContainer для сущности Department. Он может быть использован как источник элементов выпадающего списка выбора Department.
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 Отдельные поля имеют атрибуты dataContainer и property.
2 Компонент formLayout распространяет свой dataContainer на все вложенные поля, поэтому они требуют только указания атрибута property.
3 Компонент entityComboBox имеет атрибут itemsContainer для получения списка опций.
4 У таблицы dataGrid есть только атрибут dataContainer.

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

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

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

@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 помещает загруженные сущности в data context для отслеживания изменений.
5 stepsDc создается как контейнер свойства.
6 Определяем запрос для загрузчика departmentsDl.
7 ContainerValueSource используется для связи одиночных полей с контейнерами данных.
8 CollectionContainer напрямую используется для предоставления списка элементов для полей выбора.
9 ContainerDataGridItems используется для связи таблиц dataGrid с контейнерами.
10 Переопределяем getEditedEntityContainer(), чтобы указать контейнер вместо аннотации @EditedEntityContainer.
11 Загружаем данные перед отображением экрана. Идентификатор редактируемой сущности будет автоматически передан в загрузчик userDl.

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

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

dependent tables

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

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

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

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

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

<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 здесь не используется, поэтому загрузчики нужно вызвать программно в контроллере.
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 позволяет устанавливать связи между компонентами данных декларативно без написания кода на Java.

Сортировка

Визуальные компоненты, привязанные к коллекции, например dataGrid, используют порядок сортировки, определенный в связанном CollectionContainer. Jmix поддерживает как сортировку в памяти, так и сортировку на основе JPQL со стандартным поведением и предоставляет точки расширения для кастомизации.

Стандартное поведение

Сортировка работает следующим образом:

  1. Визуальный компонент вызывает Sorter, связанный с контейнером коллекции.

  2. Фреймворк получает этот сортировщик через CollectionContainerSortManager.

  3. Если ни один провайдер не вернул сортировщик, менеджер использует SorterFactory для создания стандартного CollectionContainerSorter или CollectionPropertyContainerSorter.

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

Кастомизация сортировки в памяти

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

Чтобы настроить сортировку в памяти для этой сущности:

  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, который возвращает пользовательский сортировщик только для поддерживаемых контейнеров:

    @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.

Если атрибут поддерживается, верните выражение сортировки JPQL, иначе верните null, чтобы обработку продолжил следующий поставщик:

@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;
    }
}

Поставщик может реализовать следующие методы:

  • getDatatypeSortExpression(SortExpressionContext context)

  • getLobSortExpression(SortExpressionContext context)

ExpressionOrder

Используйте Sort.ExpressionOrder, когда сортировку нельзя выразить через путь к атрибуту сущности и вместо этого нужно использовать выражение JPQL.

В отличие от обычного Sort.Order, который сортирует по атрибуту сущности, например name или customer.name, ExpressionOrder использует указанное выражение JPQL непосредственно в ORDER BY. Это полезно, когда сортировка зависит от функции, преобразования типа или другого пользовательского выражения.

Например, выражение 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, псевдоним {E} в ExpressionOrder не поддерживается. Используйте тот же псевдоним, что и в запросе. Например, для select e.name, e.startTime, e.endTime from CarService e используйте length(e.name).

Несколько провайдеров сортировки

Jmix поддерживает несколько провайдеров сортировки как для сортировки в памяти, так и для выражений сортировки JPQL, и обрабатывает их в порядке Spring:

  • Для сортировки в памяти можно зарегистрировать несколько бинов, реализующих CollectionContainerSortProvider. Если ни один провайдер не вернет сортировщик, будет использован стандартный SorterFactory.

  • Для выражений сортировки JPQL можно зарегистрировать несколько бинов, реализующих JpqlSortExpressionSupplier. Если ни один поставщик не поддерживает свойство, будет использован стандартный JpqlSortExpressionProvider.

Например, дополнение может предоставить один провайдер, а приложение - другой:

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

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

В этом примере AddonSortProvider проверяется первым, потому что у него ниже приоритет.

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