Как получить список элементов из multiSelectComboBox?

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

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<view xmlns="http://jmix.io/schema/flowui/view"
      title="msg://trafficListView.title"
      focusComponent="trafficsDataGrid">
    <data>
        <collection id="trafficsDc"
                    class="ru.ctsg.damdbf.manager.ui.dto.traffic.TrafficUiDto">
            <fetchPlan extends="_base"/>
            <loader id="trafficsDl" readOnly="true"/>
        </collection>
    </data>
    <facets>
        <dataLoadCoordinator auto="true"/>
        <urlQueryParameters>
            <pagination component="pagination"/>
        </urlQueryParameters>
    </facets>
    <actions>
        <action id="selectAction" type="lookup_select"/>
        <action id="discardAction" type="lookup_discard"/>
    </actions>
    <layout>
        <details summaryText="msg://ru.ctsg.damdbf.manager.ui.view.traffic/filter" opened="true"
                 classNames="detailsListView">
            <formLayout>
                <responsiveSteps>
                    <responsiveStep minWidth="0em" columns="3"/>
                </responsiveSteps>
                <multiSelectComboBox id="filterProcessInfoComboBox" property="processInfo"
                                     label="msg://ru.ctsg.damdbf.manager.ui.view.traffic/processInfo"
                                     width="15em" classNames="filterBox">
                </multiSelectComboBox>
                <multiSelectComboBox id="filterRequestTypeComboBox"
                                     itemsEnum="ru.ctsg.damdbf.manager.core.enums.RequestType"
                                     label="msg://ru.ctsg.damdbf.manager.ui.view.traffic/requestType" width="15em"
                                     classNames="filterBox"/>
                <multiSelectComboBox id="filterTablesNameComboBox" property="tablesName"
                                     label="msg://ru.ctsg.damdbf.manager.ui.view.traffic/tablesName" width="15em"
                                     classNames="filterBox">
                </multiSelectComboBox>
                <multiSelectComboBox id="filterAttributesNameComboBox" property="attributesName"
                                     label="msg://ru.ctsg.damdbf.manager.ui.view.traffic/attributesName" width="15em"
                                     classNames="filterBox">
                </multiSelectComboBox>
                <dateTimePicker id="filterTimestampRequestDateTimePicker" property="timestampRequest"
                                label="msg://ru.ctsg.damdbf.manager.ui.view.traffic/timestampRequest"
                                width="15em" classNames="filterBox"/>
            </formLayout>
            <hbox classNames="hboxFilterButton">
                <button id="refreshFilterBtn" action="trafficsDataGrid.refresh" classNames="refreshFilterBtn"/>
                <button id="clearFilterBtn" action="trafficsDataGrid.clear" classNames="clearFilterBtn"/>
            </hbox>
        </details>
        <dataGrid id="trafficsDataGrid"
                  width="100%"
                  minHeight="20em"
                  dataContainer="trafficsDc"
                  columnReorderingAllowed="true">
            <actions>
                <action id="refresh" type="list_refresh" text="msg://ru.ctsg.damdbf.manager.ui.action/Search"
                        icon="SEARCH"/>
                <action id="clear" text="msg://ru.ctsg.damdbf.manager.ui.action/Clear" icon="MINUS_CIRCLE_O"/>
            </actions>
            <columns resizable="true">
                <column key="icon" sortable="false" flexGrow="0" resizable="false"/>
                <column property="processInfo"/>
                <column property="agentId"/>
                <column property="requestType"/>
                <column property="tablesName"/>
                <column property="attributesName"/>
                <column property="trafficRequest"/>
                <column property="timestampRequest"/>
                <column key="contextMenu" width="10em" flexGrow="0" sortable="false"/>
            </columns>
        </dataGrid>
        <hbox alignSelf="CENTER">
            <simplePagination id="pagination" dataLoader="trafficsDl" itemsPerPageVisible="true"
                              itemsPerPageItems="10, 20, 50, 100" itemsPerPageDefaultValue="10" alignSelf="CENTER"/>
        </hbox>
        <hbox id="lookupActions" visible="false">
            <button id="selectBtn" action="selectAction"/>
            <button id="discardBtn" action="discardAction"/>
        </hbox>
    </layout>
</view>

loadDelegate в контроллере экрана переопределен, я данные достаю из БД Clickhouse:

    @Install(to = "trafficsDl", target = Target.DATA_LOADER)
    public List<TrafficUiDto> trafficsDlLoadDelegate(final LoadContext<TrafficUiDto> loadContext) {
        LoadContext.Query query = loadContext.getQuery();
        if (query == null) {
            return Collections.emptyList();
        }

        int offset = query.getFirstResult() / query.getMaxResults();
        int limit = query.getMaxResults();

        Map<String, Object> conditionsMap = processConditions();
        boolean hasNonNullValues = conditionsMap.values().stream().anyMatch(Objects::nonNull);

        List<TrafficDto> trafficDtos;
        if (hasNonNullValues) {
            trafficDtos = trafficService.getFilteredProcessedTraffic(PageRequest.of(offset, limit), conditionsMap);
        } else {
            Sort.Direction direction = getSortDirection(query.getSort());
            if (direction == null) {
                trafficDtos = trafficService.getPaginatedProcessedTraffic(PageRequest.of(offset, limit));
            } else {
                String property = getSortProperty(query.getSort());
                trafficDtos =
                        trafficService.getSortedProcessedTraffic(PageRequest.of(offset, limit), direction, property);
            }
        }

        return mapToUiDto(trafficDtos);
    }

Собственно, чтобы стандартно определить список элементов для multiSelectComboBox, нужно сделать следующее (пример взят из документации):

<multiSelectComboBox metaClass="Hobby"
                     label="Hobby"
                     pageSize="30"> 
    <itemsQuery class="com.company.onboarding.entity.Hobby"
                escapeValueForLike="true"
                searchStringFormat="(?i)%${inputString}%">
        <fetchPlan extends="_base"/>
        <query>
            <![CDATA[select e from Hobby e where e.name
            like :searchString escape '\' order by e.name]]>
        </query>
    </itemsQuery>
</multiSelectComboBox>

У меня так работать не будет. Еще извлечение элементов также может быть определено программно с помощью обработчика itemsFetchCallback (пример из документации multiSelectComboBox :: Документация Jmix). В этом примере видно, что при помощи dataManager мы извлекаем все hobbies. В документации также указано, что компонент подгружает данные как бы лениво получается. Сначала 50 по умолчанию, потом еще 50 и тд. Но если мне нужно выгрузить значения столбца из таблицы на несколько миллионов, а то и десятков-сотен миллионов, что делать тогда? Вытаскивать все эти значения и держать в памяти? Как это будет работать в моем случае? Или мне лучше данный компонент в данном кейс не использовать?

Разъясните, пожалуйста.

Добрый день,

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

Если загрузка, то можно в целом воспользоваться API комбо бокса и через CallbackDataProvider
точно также передать туда и limit и offset и запрос, который ввел туда пользователь, если вы не из базы данных загружаете данные.
image

Отвечая на ваш вопрос про логику - комбо бокс делегирует запросы коллекшен лоадеру, потому все такая же ленивая загрузка останется

С уважением,
Дмитрий

@d.cherkasov добрый день! Спасибо, что ответили.
У меня вопрос, как положить данные в multiComboBox, если их очень много (несколько миллионов значений)? Я хочу добиться аналогичного поведения, которое реализовано по дефолту для multiComboBox (вот как указано в документации):

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

Вот у меня есть работающий комбо бокс, где я задал pageSize=20

                    <multiSelectComboBox id="filterNameComboBox" property="name"
                                     label="msg://ru.ctsg.damdbf.manager.view.agent/nameFilter" pageSize="20"
                                     width="15em" classNames="filterBox">
                    <itemsQuery escapeValueForLike="true"
                                searchStringFormat="(?i)%${inputString}%">
                        <query>
                            <![CDATA[select distinct e.name from Agent e where e.name
                                 like :searchString escape '\' order by e.name asc]]>
                        </query>
                    </itemsQuery>
                </multiSelectComboBox>

в терминале при нажатии на этот комбо бокс я вижу, что отправляется два запроса в БД:
Сначала достаются первые 20 элементов:

SELECT LIMIT 0 20 DISTINCT NAME AS a1 FROM MCORE_AGENT WHERE LOWER(NAME) LIKE '%%' ESCAPE '\' ORDER BY NAME ASC

Затем еще 20 элементов и т.д.:

SELECT LIMIT 20 20 DISTINCT NAME AS a1 FROM MCORE_AGENT WHERE LOWER(NAME) LIKE '%%' ESCAPE '\' ORDER BY NAME ASC

У меня пока получилось реализовать такое поведение:
В своем контроллере экрана на уровне класса определил переменную trafficUiDtos, которая будет хранить список значений:

@Route(value = "traffics", layout = MainView.class)
@ViewController("TrafficUiDto.list")
@ViewDescriptor("traffic-ui-dto-list-view.xml")
@LookupComponent("trafficsDataGrid")
@DialogMode(width = "64em")
public class TrafficListView extends StandardListView<TrafficUiDto> {

    private List<TrafficUiDto> trafficUiDtos;

    @Subscribe
    public void onInit(final InitEvent event) {
        LoadContext<TrafficUiDto> loadContext = trafficsDl.createLoadContext();
        LoadContext.Query query = loadContext.getQuery();
        if (query != null) {
            int offset = query.getFirstResult() / query.getMaxResults();
            int limit = query.getMaxResults();
            List<TrafficDto> paginatedProcessedTraffic =
                    trafficService.getPaginatedProcessedTraffic(PageRequest.of(offset, limit));
            trafficUiDtos = mapToUiDto(paginatedProcessedTraffic);
        }
    }

    @Install(to = "filterProcessInfoComboBox", subject = "itemsFetchCallback")
    public Stream<String> filterProcessInfoComboBoxItemsFetchCallback(final Query<TrafficUiDto, String> query) {

        return trafficUiDtos.stream()
                .map(TrafficUiDto::getProcessInfo)
                .skip(query.getOffset())
                .limit(query.getLimit());
    }

В методе onInit достаю из базы элементы, используя пагинацию. В методе filterProcessInfoComboBoxItemsFetchCallback уже добавляю логику, как элементы будут отображаться в multiComboBox. Ну и сейчас всегда отображается первые 10 элементов, т.к. offset и limit, получаемые из loadContext равны 0 и 10 соответственно. Получается у меня в целом сейчас одна проблема: как положить данные в комбо бокс правильно? Потому как если я буду доставать данные из БД, не использую пагинацию, у меня спокойно в переменной trafficUiDtos может лежать несколько миллионов значений, и каждый раз при инициализации экрана и срабатывании метода onInit, я буду ходить в БД и доставать оттуда все данные, что вообще не эффективно.
Можете подсказать, как мне тут поступить? Или без получения из БД всех элементов тут не обойтись? Хотя в документации про использование того же dataManager сказано, что грузить все экземпляры можно только в том случае, если количество строк в соответствующей таблице всегда будет небольшим. В противном случае нужно использовать пагинацию.

1 симпатия

@d.cherkasov вопрос решился, написал следующий код, все работает:

    @Install(to = "filterProcessInfoComboBox", subject = "itemsFetchCallback")
    public Stream<String> filterProcessInfoComboBoxItemsFetchCallback(final Query<TrafficUiDto, String> query) {
        List<TrafficUiDto> trafficUiDtos;
        List<TrafficDto> paginatedProcessedTraffic =
                trafficService.getPaginatedProcessedTraffic(PageRequest.of(query.getOffset(), query.getLimit()));
        trafficUiDtos = mapToUiDto(paginatedProcessedTraffic);

        return trafficUiDtos.stream()
                .map(TrafficUiDto::getProcessInfo)
                .skip(query.getOffset())
                .limit(query.getLimit());
    }
1 симпатия