Использование компонентов данных
В данном разделе рассмотрены практические примеры работы с компонентами данных.
Декларативное использование
Обычно компоненты данных определяются и привязываются к визуальным компонентам декларативно в 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. |
Сортировка
Визуальные компоненты, привязанные к коллекции, например dataGrid, используют порядок сортировки, определенный в связанном CollectionContainer. Jmix поддерживает как сортировку в памяти, так и сортировку на основе JPQL со стандартным поведением и предоставляет точки расширения для кастомизации.
Стандартное поведение
Сортировка работает следующим образом:
-
Визуальный компонент вызывает
Sorter, связанный с контейнером коллекции. -
Фреймворк получает этот сортировщик через
CollectionContainerSortManager. -
Если ни один провайдер не вернул сортировщик, менеджер использует
SorterFactoryдля создания стандартногоCollectionContainerSorterилиCollectionPropertyContainerSorter.
Стандартный сортировщик сортирует данные в памяти, если у контейнера нет загрузчика или если загружена только первая страница и количество загруженных элементов меньше размера страницы. В противном случае он повторно загружает данные из базы данных с запрошенной сортировкой. Подробности о JPQL-сортировке см. в разделе Кастомизация выражений сортировки JPQL.
Кастомизация сортировки в памяти
Некоторым атрибутам сущностей может потребоваться особая логика сортировки. Например, у сущности Department может быть атрибут num типа String, хотя фактически в нем хранятся числовые значения. При сортировке как строк получится порядок 1, 10, 11, 2, 3, а при числовой сортировке - более естественный порядок: 1, 2, 3, 10, 11.
Чтобы настроить сортировку в памяти для этой сущности:
-
Создайте подкласс
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); } } -
Создайте бин, реализующий
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); } } -
Привяжите контейнер к табличному компоненту, например 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 необходимо задать самостоятельно.
|
Если запрос возвращает |
Несколько провайдеров сортировки
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 проверяется первым, потому что у него ниже приоритет.
| Бин с низким приоритетом может выступать в роли стандартного для всего приложения, при этом более специфичные бины все равно смогут его переопределить. |