Как правильно использовать CollectionContainer при вложенных коллекциях данных

Добрый день!
Есть сущность ServiceOrder, которая включает в себя коллекцию сущностей ServiceOrderItem, которая в свою очередь включает в себя коллекцию сущностей ServiceOrderItemDetail

Необходимо сделать две таблицы:
в одну вывести список ServiceOrderItem
в другую список ServiceOrderItemDetail

Загружаю данные о ServiceOrder так

    <instance id="serviceOrderDc"
              class="ServiceOrder" view="serviceOrder-view">
        <loader/>
        <collection id="serviceOrderItemsDc" property="serviceOrderItems">
            <collection id="serviceOrderItemDetailsDc" property="serviceOrderItemDetails">
            </collection>
        </collection>
    </instance>

с первой таблицей проблем нет, но во второй когда добавляю колонки из ServiceOrderItemDetail

            <groupTable id="serviceOrdersTableDetail" height="auto" width="100%"
                        dataContainer="serviceOrderItemsDc"
                        editable="true">
                <columns>
                    <group>
                        <column id="serviceGroup.name"/>
                    </group>
                    <column id="serviceOrderItemDetails.quantityFact" editable="true"/>
                </columns>
            </groupTable>

при раскрытии таблицы появляется ошибка:
IllegalArgumentException: Cannot format given Object as a Number

Если указываю dataContainer=“serviceOrderItemDetailsDc”, то список ServiceOrderItemDetail отображается корректно, но только по тому ServiceOrderItem, который выделен на первой таблице. А нужно, чтобы список был по всем ServiceOrderItem.

Пыталась сделать загрузку списка ServiceOrderItemDetail отдельным лоадером

    <collection id="serviceOrderItemDetailsFull"
                class="com.elgsys.wld.entity.ServiceOrderItemDetail" view="serviceOrderItemDetail-view">
        <loader id="serviceOrderItemDetailsFullDl">
            <query>
                <![CDATA[select e from ServiceOrderItemDetail e]]>
                <condition>
                    <and>
                        <jpql>
                            <where>e.serviceOrderItem.serviceOrder = :serviceOrder</where>
                        </jpql>
                    </and>
                </condition>
            </query>
        </loader>
    </collection>
</data>

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

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

Изменения такие: в зависимости от значения поля ServiceOrderItem(количество) создается коллекция ServiceOrderItemDetail и так для каждого ServiceOrderItem. Как создать коллекцию я знаю, вопрос в том, как ее отобразить во второй таблице.

Нужно примерно так:
Первая таблица:

ServiceOrderItem1
ServiceOrderItem2

Вторая таблица:

ServiceOrderItem1(количество=2)
ServiceOrderItemDetail11
ServiceOrderItemDetail12
ServiceOrderItem2(количество=1)
ServiceOrderItemDetail21

Заранее спасибо!

Декларативно это сделать не получится, потребуется программировать.
Пара вопросов:

  1. Как все это хозяйство должно сохраняться? Все вместе с владеющим Order или по-отдельности? Например если вы добавляете или удаляете записи в таблицах?
  2. Что означает наличие ServiceOrderItem1(количество=2) во второй таблице которая отображает ServiceOrderItemDetail?
  1. Должно сохраняться вместе с ServiceOrder. Идея такая - пользователь открыл экран, внес изменения в ServiceOrder, включающий ServiceOrderItem и ServiceOrderItemDetail, если нажал “Сохранить”, то изменения сохранились, если нет, то не сохранились.
    2.Это попытка описать вложенность, нужно, чтобы было так
    Безымянный

2022-07-29_161817

Попробовала сама, немного получилось.
Добавила контейнер

    <collection id="serviceOrderItemDetailContainer" class="ServiceOrderItemDetail">
        <view extends="serviceOrderItemDetail-view">
        </view>
    </collection>

В него пишу данные при открытии формы

    List<ServiceOrderItemDetail> serviceOrderItemDetailAll = new ArrayList<>();
    for (ServiceOrderItem serviceOrderItem : serviceOrderItemsDc.getItems()) {
        serviceOrderItemDetailAll.addAll(serviceOrderItem.getServiceOrderItemDetails());
    }
    serviceOrderItemDetailContainer.setItems(serviceOrderItemDetailAll);
    TableItems<ServiceOrderItemDetail> tableItems = new ContainerGroupTableItems<>(serviceOrderItemDetailContainer);
    serviceOrdersTableDetail.setItems(tableItems);

serviceOrderItemsDc - это коллекция основного контейнера.

Работает, но не могу корректно сохранить.
А конкретно - очистить коллекцию. Причем раньше я этот элемент в коллекцию успешно добавила, а вот теперь обнулить не могу.

public void onBeforeCommitChanges(BeforeCommitChangesEvent event) {
    List<ServiceOrderItem> serviceOrderItems = this.getEditedEntity().getServiceOrderItems();
    List<ServiceOrderItemDetail> serviceOrderItemDetails = serviceOrderItemDetailContainer.getItems();

    for (ServiceOrderItem serviceOrderItem : serviceOrderItems) {
        serviceOrderItem.setServiceOrderItemDetails(new ArrayList<>());

        for (ServiceOrderItemDetail serviceOrderItemDetail : serviceOrderItemDetails) {
            if (serviceOrderItemDetail.getServiceOrderEmployee() == null)
                continue;
            if (serviceOrderItemDetail.getServiceOrderItem().getId() == serviceOrderItem.getId()) {
                dataContext.merge(serviceOrderItemDetail);
                serviceOrderItem.getServiceOrderItemDetails().add(serviceOrderItemDetail);
            }
        }
    }
    this.getEditedEntity().setServiceOrderItems(serviceOrderItems);
}

Строка почему-то не срабатывает, подскажите, как можно обнулить коллекцию в данном случае?

serviceOrderItem.setServiceOrderItemDetails(new ArrayList<>());

Здравствуйте!
У вас на экране есть контейнер serviceOrderItemDetailsDc, который связан с атрибутом редактируемой сущности property=“serviceOrderItemDetails”. Вы в событии BeforeCommitChanges меняете атрибут serviceOrderItemDetails, а затем во время коммита в этот атрибут сохраняется содержимое того самого контейнера serviceOrderItemDetailsDc.
Не хочу показаться занудой :nerd_face:, но подумайте над тем, какие атрибуты редактируемой сущности связаны с компонентами экрана(и контейнерами) и как лучше в одностороннем порядке рулить атрибутами сущности. :grinning: С обрастанием логикой разобраться в работе экранов будет сложнее :laughing:

Добрый день, Сергей!
Спасибо за ответ, посмотрела в сторону контейнера serviceOrderItemDetailsDc. Пытаюсь записать в него данные из serviceOrderItemDetailContainer, но получаю ошибку и если обращаюсь к нему таким образом:

serviceOrderItemDetailsDc.getMutableItems().clear();

и если через переменную

List<ServiceOrderItemDetail> list = serviceOrderItemDetailsDc.getMutableItems();
list.add(serviceOrderItemDetail);

Подскажите, пожалуйста, в правильном ли направлении я думаю, и если в правильном, то в чем может быть причина ошибки, ошибка следующая

IllegalStateException: Current item is null

Вы правильно сделали что добавили отдельный контейнер serviceOrderItemDetailContainer и связали с таблицей. И в этом случае вам не нужен вложенный serviceOrderItemDetailsDc, контейнеры нужны только для связи с визуальными компонентами.

Мне кажется что все изменения в сущностях контейнера serviceOrderItemDetailContainer должны сохраниться автоматически при коммите экрана, так как все они уже находятся в DataContext и он отслеживает их изменения.
Без реального кода под руками конечно сложно судить на 100%, но пока некогда делать такой пример. Если вы предоставите тестовый проект - будет здорово.

Добрый день!
@krivopustov прав. Я ввел вас в заблуждение с контейнерами. Прошу прощения.
У меня получилось обновить коллекцию (сначала обнулить, потом заполнить) только удалив связь у ServiceOrderItemDetail на serviceOrderItem, при условии, что у меня serviceOrderItem связана с ServiceOrderItemDetail как ASSOCIATION oneToMany.

//Вариант через контейнеры
        //коллекция удаляемых serviceOrderItemDetails
        List<ServiceOrderItemDetail> removedItemDetails =
                serviceOrderItemDetailsDc.getItems()
                        .stream()
                        .filter(serviceOrderItemDetail -> serviceOrderItemDetail.getName().contains(filterWord))
                        .collect(Collectors.toList());
        //удаление связи serviceOrderItemDetail с serviceOrderItem
        removedItemDetails.forEach(serviceOrderItemDetail -> serviceOrderItemDetail.setServiceOrderItem(null));
        //обновление контейнера
        serviceOrderItemDetailsDc.getMutableItems().removeAll(removedItemDetails);
        //обновление serviceOrderItem
        for (ServiceOrderItem serviceOrderItem : serviceOrderItemsDc.getMutableItems()){
            serviceOrderItem.getServiceOrderItemDetails().clear();
            for (ServiceOrderItemDetail serviceOrderItemDetail : serviceOrderItemDetailsDc.getMutableItems()){
                if(serviceOrderItemDetail.getServiceOrderItem().equals(serviceOrderItem)){
                    serviceOrderItem.getServiceOrderItemDetails().add(serviceOrderItemDetail);
                }
            }
            //обновление контейнера
            serviceOrderItemsDc.replaceItem(serviceOrderItem);
        }

или через атрибуты сущности:

//Вариант через атрибуты
        List<ServiceOrderItem> serviceOrderItems = getEditedEntity().getServiceOrderItems();

        List<ServiceOrderItemDetail> serviceOrderItemDetails = new ArrayList<>(Collections.emptyList());
        for(ServiceOrderItem serviceOrderItem : serviceOrderItems){
            serviceOrderItemDetails.addAll(serviceOrderItem.getServiceOrderItemDetails());
        }

        List<ServiceOrderItemDetail> filteredItemDetails =
                serviceOrderItemDetails.stream()
                        .filter(serviceOrderItemDetail -> serviceOrderItemDetail.getName().contains(filterWord))
                        .collect(Collectors.toList());

        filteredItemDetails.forEach(serviceOrderItemDetail -> serviceOrderItemDetail.setServiceOrderItem(null));
        serviceOrderItemDetails.removeAll(filteredItemDetails);

        List<ServiceOrderItem> updatedServiceOrderItems = new ArrayList<>(Collections.emptyList());
        for (ServiceOrderItem serviceOrderItem : serviceOrderItems){
            serviceOrderItem.getServiceOrderItemDetails().clear();
            for (ServiceOrderItemDetail serviceOrderItemDetail : serviceOrderItemDetails){
                if(serviceOrderItemDetail.getServiceOrderItem().equals(serviceOrderItem)){
                    serviceOrderItem.getServiceOrderItemDetails().add(serviceOrderItemDetail);
                }
            }
            updatedServiceOrderItems.add(serviceOrderItem);
        }
        getEditedEntity().setServiceOrderItems(updatedServiceOrderItems);

Контекст: удаляем serviceOrderItemDetail у serviceOrderItem где serviceOrderItemDetail.Name().contains(filterWord). У вас serviceOrderItemDetail.getServiceOrderEmployee() == null.

Надеюсь понятно объяснил. :grinning: Коллеги поправят, если я не прав.

@krivopustov, у экрана указан контейнер serviceOrderDc, и его содержимое сохранятся при коммите

EditedEntityContainer(“serviceOrderDc”)

Спасибо за ответ, проект подготовлю.

@sas Спасибо за варианты решения, но есть один нюанс - нужно вставить записи из другого контейнера и тут возникают проблемы

Вот такой код
for (ServiceOrderItem serviceOrderItem : serviceOrderItemsDc.getMutableItems()) {
serviceOrderItem.getServiceOrderItemDetails().clear();

        for (ServiceOrderItemDetail serviceOrderItemDetail : serviceOrderItemDetailContainer.getMutableItems()) {
            if (serviceOrderItemDetail.getServiceOrderEmployee() == null)
                continue;
            if (serviceOrderItemDetail.getServiceOrderItem().equals(serviceOrderItem)) {
                serviceOrderItem.getServiceOrderItemDetails().add(serviceOrderItemDetail);
            }
        }
    }

вызывает ошибку

IllegalStateException: An attempt to save an entity with reference to some not persisted entity. All newly created entities must be saved in the same transaction. Put all these objects to the CommitContext before commit.

а если добавить dataContext.merge(serviceOrderItemDetail);

    for (ServiceOrderItem serviceOrderItem : serviceOrderItemsDc.getMutableItems()) {
        serviceOrderItem.getServiceOrderItemDetails().clear();

        for (ServiceOrderItemDetail serviceOrderItemDetail : serviceOrderItemDetailContainer.getMutableItems()) {
            if (serviceOrderItemDetail.getServiceOrderEmployee() == null)
                continue;
            if (serviceOrderItemDetail.getServiceOrderItem().equals(serviceOrderItem)) {
                **dataContext.merge(serviceOrderItemDetail);**
                serviceOrderItem.getServiceOrderItemDetails().add(serviceOrderItemDetail);
            }
        }

то сохраняются и те записи, которые были удалены из контейнера и те, которые были добавлены.

Причина такого поведения у меня было наличие в удаленных(не попадающих в ServiceOrderItem по условию) ServiceOrderItemDetail’ов ссылочного атрибута ServiceOrderItemDetail.ServiceOrderItem, который их связывает. Ведь связь храниться именно в details’ах. Т.к. контейнеры ItemsDc и DetailsDc не связаны декларативно, то наверно нужно “подчищать” связи. Может есть какая-то аннотация для атрибута или иной механизм, но мне помог этот вариант.

//удаление связи serviceOrderItemDetail с serviceOrderItem
        removedItemDetails.forEach(serviceOrderItemDetail -> serviceOrderItemDetail.setServiceOrderItem(null));

@krivopustov
Добрый день!
Прикладываю архив с проектом.
Логика такая:

  1. Пользователь нажимает кнопку “создать”, открывается экран редактирования, он работает с первой таблицей, сохраняет. В тестовом проекте можно просто открыть экран и сразу сохранить, все необходимые данные сгенирируются.
  2. После этого нажимает кнопку “редактировать”, открывается экран, первая таблица недоступна для редактирования. Пользователь работает со второй - добавляет записи(сущности ServiceOrderItemDetail), сохраняет экран. В тестовом проекте можно открыть экран редактирования и сохранить, все необходимые данные сгенирируются.
  3. Если после этого снова открыть форму на редактирование, изменить список сущностей во второй таблице(в приложенном проекте это реализовано нажатием на кнопку “Удалить serviceOrderItem_2_Имя_0 и добавить serviceOrderItem_2_Имя_3”) и сохранить, то добавленные записи сохранятся, а удаленные останутся.
    Подскажите, пожалуйста, как сделать так, чтобы при редактировании списка сущностей ServiceOrderItemDetail он корректно сохранялся в базу?
    Заранее спасибо!
    testcollection.zip (164.8 КБ)

Татьяна, спасибо за тестовый проект - так гораздо проще разобраться.

Вам необходимо после удаления сущности из контейнера вызвать удаление этого экземпляра из DataContext:

ServiceOrderItem serviceOrderItem = serviceOrderItemDetails.get(3).getServiceOrderItem();
ServiceOrderItemDetail removedDetail = serviceOrderItemDetails.remove(3);
// notify DataContext that this instance must be removed from database
dataContext.remove(removedDetail);

Тогда при коммите экрана DataContext отправит этот экземпляр на удаление из БД.

1 симпатия

Константин, спасибо вам огромное! Получилось сохранить так, как надо.