Использование компонентов данных
В данном разделе рассмотрены практические примеры работы с компонентами данных.
Декларативное использование
Обычно компоненты данных определяются и привязываются к визуальным компонентам декларативно в 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 . |
Зависимости между компонентами данных
Иногда требуется загружать и отображать данные, которые зависят от других данных в том же экране. К примеру, на скриншоте ниже таблица слева отображает список сотрудников, а таблица справа – список оборудования для выбранного сотрудника. Список справа обновляется каждый раз, когда меняется выбранный элемент в таблице слева.
В этом примере сущность 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);
}
}