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

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

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

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

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

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

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

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

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

<data readOnly="true">
    <collection id="customersDc"
                class="ui.ex1.entity.Customer">
        <fetchPlan extends="_base"/>
        <loader id="customersDl" >
            <query>
                <![CDATA[select e from uiex1_Customer e]]>
            </query>
        </loader>
    </collection>
</data>
<layout expand="customersTable" spacing="true">
    <filter id="filter"
            dataLoader="customersDl">
        <properties include=".*"/>
    </filter>
    <!-- ... -->
</layout>

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

<data>
    <instance id="customerDc"
              class="ui.ex1.entity.Customer">
        <fetchPlan extends="_base"/>
        <loader/>
    </instance>
</data>

Загрузчики также можно создавать и настраивать программно, например:

@Autowired
private DataComponents dataComponents;

private CollectionLoader<Customer> customersDl;

private void createCustomerLoader(CollectionContainer<Customer> container) {
    customersDl = dataComponents.createCollectionLoader();
    customersDl.setQuery("select e from uiex1_Customer e");
    customersDl.setContainer(container);
    customersDl.setDataContext(getScreenData().getDataContext());
}

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

События и слушатели

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

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

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

loadDelegate

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

@Autowired
private CustomerService customerService;

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

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

Если вы объявляете собственную загрузку данных с помощью делегата и отображаете загруженные данные в таблице с помощью компонента разбивки на страницы (Pagination или SimplePagination), то вам также может потребоваться определить собственную логику для подсчета общего количества строк. Обратите внимание на слушателя TotalCountDelegate компонента разбивки на страницы, связанного с таблицей.

PreLoadEvent

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

@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
public void onCustomersDlPreLoad(CollectionLoader.PreLoadEvent<Customer> event) {
    // some actions before loading
}

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

PostLoadEvent

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

@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
public void onCustomersDlPostLoad(CollectionLoader.PostLoadEvent<Customer> event) {
    // some actions after loading
}

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

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

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

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

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

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

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

@Autowired
private CollectionLoader<Person> personsDl;

@Subscribe("nameFilterField")
public void onNameFilterFieldValueChange1(HasValue.ValueChangeEvent event) {
    if (event.getValue() != null) {
        personsDl.setParameter("name", "(?i)%" + event.getValue() + "%");
    } else {
        personsDl.removeParameter("name");
    }
    personsDl.load();
}

@Subscribe("statusFilterField")
public void onStatusFilterFieldValueChange(HasValue.ValueChangeEvent<Boolean> event) {
    if (event.getValue()) {
        personsDl.setParameter("status", true);
    } else {
        personsDl.removeParameter("status");
    }
    personsDl.load();
}

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

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