Использование компонентов данных
В данном разделе рассмотрены практические примеры работы с компонентами данных.
Декларативное использование
Обычно компоненты данных определяются и привязываются к визуальным компонентам декларативно в 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
) выбранного сотрудника. Список справа обновляется каждый раз, когда меняется выбранный элемент в таблице слева.
В этом примере сущность 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. |
Специализированная сортировка
Сортировка таблиц по атрибутам сущности в UI производится объектом типа CollectionContainerSorter
, который устанавливается для CollectionContainer. Стандартная реализация сортирует данные в памяти, если загруженный список умещается на одной странице, или посылает запрос с соответствующим "order by" в базу данных. Выражение "order by" формируется бином JpqlSortExpressionProvider
.
Некоторые атрибуты могут потребовать специальной реализации сортировки. Ниже рассматривается простой пример: предположим, в сущности Department
есть атрибут num
типа String
, но на самом деле атрибут хранит только числовые значения. Поэтому необходимо иметь порядок сортировки для чисел: 1
, 2
, 3
, 10
, 11
. Стандартный механизм сортировки в данном случае выдаст порядок 1
, 10
, 11
, 2
, 3
.
Сначала создайте наследника класса CollectionContainerSorter
для сортировки в памяти:
package com.company.onboarding.app;
import com.company.onboarding.entity.Department;
import io.jmix.core.Sort;
import io.jmix.core.metamodel.model.MetaClass;
import io.jmix.core.metamodel.model.MetaPropertyPath;
import io.jmix.flowui.model.BaseCollectionLoader;
import io.jmix.flowui.model.CollectionContainer;
import io.jmix.flowui.model.impl.CollectionContainerSorter;
import io.jmix.flowui.model.impl.EntityValuesComparator;
import org.springframework.beans.factory.BeanFactory;
import java.util.Comparator;
import java.util.Objects;
public class CustomCollectionContainerSorter extends CollectionContainerSorter {
public CustomCollectionContainerSorter(CollectionContainer<?> container,
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(Department.class)
&& "num".equals(metaPropertyPath.toPathString())) {
boolean isAsc = sort.getOrders().get(0).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(sort, metaClass);
}
}
Создайте сортировщик в нужном экране:
public class DepartmentListView2 extends StandardListView<Department> {
@ViewComponent
private CollectionContainer<Department> departmentsDc;
@ViewComponent
private CollectionLoader<Department> departmentsDl;
@Autowired
private BeanFactory beanFactory;
@Subscribe
public void onInit(final InitEvent event) {
Sorter sorter = new CustomCollectionContainerSorter(departmentsDc, departmentsDl, beanFactory);
departmentsDc.setSorter(sorter);
}
Если же специализированная сортировка должна являться глобальной, то создайте собственную фабрику, которая будет инстанциировать сортировщик для всей системы:
package com.company.onboarding.app;
import io.jmix.flowui.model.BaseCollectionLoader;
import io.jmix.flowui.model.CollectionContainer;
import io.jmix.flowui.model.Sorter;
import io.jmix.flowui.model.SorterFactory;
import org.springframework.lang.Nullable;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Primary
@Component
public class CustomSorterFactory extends SorterFactory {
@Override
public Sorter createCollectionContainerSorter(CollectionContainer container,
@Nullable BaseCollectionLoader loader) {
return new CustomCollectionContainerSorter(container, loader, beanFactory);
}
}
Кроме того, можно создать собственную реализацию JpqlSortExpressionProvider
для сортировки на уровне базы данных:
package com.company.onboarding.app;
import com.company.onboarding.entity.Department;
import io.jmix.core.metamodel.model.MetaPropertyPath;
import io.jmix.data.impl.DefaultJpqlSortExpressionProvider;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Primary
@Component
public class CustomSortExpressionProvider extends DefaultJpqlSortExpressionProvider {
@Override
public String getDatatypeSortExpression(MetaPropertyPath metaPropertyPath, boolean sortDirectionAsc) {
if (metaPropertyPath.getMetaClass().getJavaClass().equals(Department.class)
&& "num".equals(metaPropertyPath.toPathString())) {
return String.format("CAST({E}.%s BIGINT)", metaPropertyPath);
}
return String.format("{E}.%s", metaPropertyPath);
}
}