Загрузчики данных

Загрузчики предназначены для загрузки данных в контейнеры данных.

Интерфейсы загрузчиков немного отличаются в зависимости от типа контейнера, с которым они работают:

  • InstanceLoader загружает единственный экземпляр сущности в контейнер InstanceContainer по идентификатору сущности или с помощью JPQL-запроса.

  • CollectionLoader загружает коллекцию сущностей в CollectionContainer с помощью JPQL-запроса. Для этого загрузчика можно настроить пейджинг, сортировку и другие дополнительные параметры.

  • KeyValueCollectionLoader загружает коллекцию экземпляров KeyValueEntity в контейнер KeyValueCollectionContainer. Кроме параметров, доступных для CollectionLoader, вы также можете указать имя хранилища данных.

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

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

Обычно загрузчик коллекций получает запрос JPQL из XML-дескриптора экрана, создает объект LoadContext и вызывает DataManager для загрузки сущностей. В итоге типичный XML-дескриптор выглядит подобным образом:

<collection id="departmentsDc"
            class="com.company.onboarding.entity.Department">
    <fetchPlan extends="_base">
        <property name="hrManager" fetchPlan="_base"/>
    </fetchPlan>
    <loader id="departmentsDl">
        <query>
            <![CDATA[select e from Department e]]>
        </query>
    </loader>
</collection>

В экране деталей сущности XML-элемент loader обычно пуст, так как для загрузки единственного экземпляра сущности требуется ее идентификатор, который устанавливается программно классом StandardDetailView:

<instance id="departmentDc"
          class="com.company.onboarding.entity.Department">
    <fetchPlan extends="_base">
        <property name="hrManager" fetchPlan="_base"/>
    </fetchPlan>
    <loader id="departmentDl"/>
</instance>

Если для загрузчика установлен DataContext (как всегда бывает в случае, если загрузчик задан в XML-дескрипторе), все загруженные сущности будут автоматически помещены в data context.

События и обработчики

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

Чтобы сгенерировать заглушку обработчика в Jmix Studio, выберите элемент данных в XML-коде дескриптора экрана или на панели иерархии Jmix UI и используйте вкладку Handlers панели инспектора Jmix UI.

В качестве альтернативы вы можете воспользоваться кнопкой Generate Handler на верхней панели контроллера экрана.

loadDelegate

Загрузчики могут делегировать фактическую загрузку методу контроллера экрана, в котором можно вызвать какой-либо сервис вместо используемого по умолчанию DataManager. Например:

@Autowired
private DepartmentService departmentService;

@Install(to = "departmentsDl", target = Target.DATA_LOADER) (1)
private List<Department> departmentsDlLoadDelegate(final LoadContext<Department> loadContext) { (2)
    LoadContext.Query query = loadContext.getQuery();
    return departmentService.loadDepartments( (3)
            query.getCondition(),
            query.getSort(),
            query.getFirstResult(),
            query.getMaxResults()
    );
}
1 Метод departmentsDlLoadDelegate() используется загрузчиком departmentsDl для получения списка экземпляров сущности Department.
2 Метод принимает LoadContext, который будет создан загрузчиком на основе его параметров: запрос, условия (при наличии) и т.д.
3 Загрузка осуществляется методом DepartmentService.loadDepartments(), который принимает условия фильтрации, сортировку и пейджинг, установленные загрузчику визуальными компонентами экрана.

Помимо вызова собственных сервисов, в делегате можно выполнить пост-обработку загруженных сущностей.

PreLoadEvent

Это событие отправляется перед загрузкой сущностей.

@Subscribe(id = "departmentsDl", target = Target.DATA_LOADER)
public void onDepartmentsDlPreLoad(final CollectionLoader.PreLoadEvent<Department> event) {
    // some actions before loading
}

Загрузку можно предотвратить, используя метод события preventLoad().

PostLoadEvent

Это событие отправляется после того, как сущности успешно загружены, помещены в DataContext и установлены в контейнер данных.

@Subscribe(id = "departmentsDl", target = Target.DATA_LOADER)
public void onDepartmentsDlPostLoad(final CollectionLoader.PostLoadEvent<Department> event) {
    // some actions after loading
}

Условия запросов

Иногда необходимо изменить запрос загрузчика данных во время выполнения программы для того, чтобы отфильтровать загружаемые данные на уровне БД. Простейший способ фильтрации в зависимости от параметров, вводимых пользователем – это подключить к загрузчику визуальные компоненты propertyFilter или genericFilter.

Вместо использования специальных компонентов-фильтров, или в дополнение к ним, для запроса в загрузчике можно задать набор условий. Условие представляет собой набор фрагментов запросов с параметрами. Эти фрагменты будут добавлены в результирующий запрос, только если все параметры, используемые во фрагментах, заданы для запроса (не null).

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

Рассмотрим создание условий для фильтрации сущности Department по ее атрибутам name и hrManager.

Условия запроса для загрузчика могут быть заданы либо декларативно в XML-элементе <condition>, либо программно методом setCondition(). Ниже приведен пример описания условий в XML:

<view xmlns="http://jmix.io/schema/flowui/view"
      xmlns:c="http://jmix.io/schema/flowui/jpql-condition"> (1)
    <data readOnly="true">
        <collection id="departmentsDc"
                    class="com.company.onboarding.entity.Department">
            <fetchPlan extends="_base">
                <property name="hrManager" fetchPlan="_base"/>
            </fetchPlan>
            <loader id="departmentsDl">
                <query>
                    <![CDATA[select e from Department e]]>
                    <condition> (2)
                        <and> (3)
                            <c:jpql> (4)
                                <c:where>e.name like :name</c:where>
                            </c:jpql>
                            <c:jpql>
                                <c:where>e.hrManager = :hrManager</c:where>
                            </c:jpql>
                        </and>
                    </condition>
                </query>
            </loader>
        </collection>
1 Добавление namespace для JPQL-условий.
2 Добавление элемента condition внутри query.
3 Если необходимо задать более одного условия, добавляется элемент and или or.
4 Задание JPQL-условие с опциональным элементом join и обязательным where.

Предположим, что в экране имеется два UI-компонента для ввода параметров условий: текстовое поле nameFilterField и выпадающий список hrManagerFilterField:

<textField id="nameFilterField" label="Name"/>
<entityComboBox id="hrManagerFilterField" label="HR Manager"
                metaClass="User" itemsContainer="usersDc">
    <actions>
        <action id="entityClear" type="entity_clear"/>
    </actions>
</entityComboBox>

Для того чтобы обновить данные, когда пользователь изменяет значения в этих компонентах, добавим следующие подписки на события в контроллере экрана:

@ViewComponent
private CollectionLoader<Department> departmentsDl;

@Subscribe("nameFilterField")
public void onNameFilterFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<TextField, String> event) {
    departmentsDl.setParameter("name", "(?i)%" + event.getValue() + "%");
    departmentsDl.load();
}

@Subscribe("hrManagerFilterField")
public void onHrManagerFilterFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<EntityComboBox<User>, User> event) {
    departmentsDl.setParameter("hrManager", event.getValue());
    departmentsDl.load();
}

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

Только для nameFilterField установлено значение
select e from Department e where e.name like :name
Только для hrManagerFilterField установлено значение
select e from Department e where e.hrManager = :hrManager
И для nameFilterField, и для hrManagerFilterField установлены значения
select e from Department e where (e.name like :name) and (e.hrManager = :hrManager)