Как вывести данные из отношения "многие ко многим" в динамическом списке 1С без дублирования строк основного документа?

Программист 1С v8.3 (Управляемые формы)
← На главную

При работе с динамическими списками в 1С часто возникает задача отображения связанных данных. Особенно сложной ситуация становится, когда речь идет об отношении "многие ко многим" между основным документом списка и подчиненными (или связанными) объектами. Типичный сценарий: на форме списка документа необходимо показать информацию о подчиненных документах, но при этом один основной документ может быть связан со множеством подчиненных, и наоборот, один подчиненный может быть связан со множеством основных. Попытка решить такую задачу напрямую, через сложные объединения в запросе динамического списка, часто приводит к нежелательному дублированию строк основного документа и значительному снижению производительности. Рассмотрим, как эффективно и корректно справиться с этой проблемой.

В чем заключается проблема отношения "многие ко многим" в динамическом списке?

Предположим, у нас есть документ "м12_ППЗ" и подчиненный ему документ "э_ПозицияПЗ". Связь между ними хранится в регистре сведений "м12_СвязанныеДокументы", где одно поле (ДокументИсточник) ссылается на "м12_ППЗ", а другое (ПодчиненныйДокумент) — на "э_ПозицияПЗ". Если каждый "м12_ППЗ" может иметь несколько "э_ПозицияПЗ", и каждая "э_ПозицияПЗ" может быть связана с несколькими "м12_ППЗ", мы сталкиваемся с классической связью "многие ко многим".

Если мы попытаемся объединить основной запрос динамического списка с регистром сведений "м12_СвязанныеДокументы" напрямую с помощью ЛЕВОЕ СОЕДИНЕНИЕ, то для каждого связанного подчиненного документа система создаст отдельную строку для исходного документа. Например, если у документа "ППЗ-000001" есть три подчиненных документа "ПозицияПЗ-001", "ПозицияПЗ-002", "ПозицияПЗ-003", то в динамическом списке мы увидим три строки с документом "ППЗ-000001", каждая из которых будет ссылаться на один из подчиненных документов.

Даже использование ключевого слова ВЫБРАТЬ РАЗЛИЧНЫЕ в запросе не всегда решает проблему полностью, так как если в выборке присутствуют другие поля, которые отличаются для каждой связанной записи (например, дата или номер подчиненного документа), строки все равно будут считаться различными и отображаться как дубликаты. Помимо визуальной путаницы, такой подход крайне неэффективен с точки зрения производительности:

  1. Избыточная выборка данных: СУБД приходится выбирать и обрабатывать значительно больший объем данных, чем необходимо для отображения уникальных строк основного документа.
  2. Нагрузка на СУБД: Операции по удалению дубликатов (если они все же возможны) также являются ресурсоемкими.
  3. Проблемы с сортировкой и фильтрацией: Логика сортировки и фильтрации может работать некорректно или медленно из-за скрытых дубликатов.

Эффективное решение: использование обработчика "ПриПолученииДанныхНаСервере"

Для решения этой проблемы в управляемых формах 1С мы рекомендуем использовать комбинацию "плоского" запроса динамического списка и обработчика события динамического списка ПриПолученииДанныхНаСервере. Этот подход позволяет:

  1. Сохранить уникальность строк основного документа: Основной запрос динамического списка выбирает только уникальные строки главного документа, не умножая их.
  2. Оптимизировать производительность: Дополнительные данные запрашиваются только для тех строк, которые реально отображаются на экране пользователя, а не для всей базы.
  3. Гибко управлять отображением: Мы можем агрегировать или форматировать связанные данные так, как нам необходимо, с помощью кода на стороне сервера.

Рассмотрим этот подход по шагам.

Шаг 1: Подготовка запроса динамического списка

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

Например, если вы хотите вывести номер подчиненного документа, добавьте в запрос поле-заглушку:


|ВЫБРАТЬ
| док.Ссылка,
| док.Номер,
| док.Дата,
| // ... другие поля основного документа ...
| ВЫРАЗИТЬ("" КАК СТРОКА(200)) КАК НомераПодчиненныхДокументов
|ИЗ
| Документ.м12_ППЗ КАК док

Обратите внимание, что мы используем ВЫРАЗИТЬ("" КАК СТРОКА(200)), чтобы создать поле типа "Строка" заданной длины. Это важно для корректного отображения и избежания ошибок типов при заполнении поля в дальнейшем. Если бы вы хотели, например, получить сумму, то использовали бы ВЫРАЗИТЬ(0 КАК ЧИСЛО).

Шаг 2: Реализация обработчика "ПриПолученииДанныхНаСервере"

Основная логика получения и заполнения связанных данных будет реализована в процедуре обработчика события динамического списка ПриПолученииДанныхНаСервере. Эта процедура вызывается каждый раз, когда динамический список запрашивает данные для отображения (например, при прокрутке или изменении фильтров).

Подробно разберем структуру и логику этой процедуры:


&НаСервереБезКонтекста
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)

    // Проверяем, выведено ли поле на форму. 
    // Это опционально, но помогает избежать лишних действий, если колонка не нужна.
    Для Каждого КлючИЗначение Из Строки Цикл
        // Проверяем наличие одного из заполняемых полей, например "НомераПодчиненныхДокументов"
        Если Не КлючИЗначение.Значение.Данные.Свойство("НомераПодчиненныхДокументов") Тогда
            Возврат; // Если поля нет на форме, выходим из процедуры
        КонецЕсли;
        Прервать; // Достаточно проверить один элемент
    КонецЦикла;

    // 1. Формируем массив ссылок на основные документы, которые видны в списке
    МассивСсылокОсновногоДокумента = Новый Массив;
    СоответствиеСтрок = Новый Соответствие; // Используем для быстрого доступа к строке списка по ссылке

    Для Каждого КлючИЗначение Из Строки Цикл
        // Строки.ПолучитьКлючи возвращает массив ссылок только основного типа динамического списка.
        // Но здесь мы явно перебираем Строки и берем КлючИЗначение.Ключ.Ссылка
        // это будет ссылка на документ "м12_ППЗ"
        СсылкаНаДокумент = КлючИЗначение.Ключ.Ссылка; 
        МассивСсылокОсновногоДокумента.Добавить(СсылкаНаДокумент);
        СоответствиеСтрок.Вставить(СсылкаНаДокумент, КлючИЗначение.Значение);
    КонецЦикла;

    // Если массив пуст (например, список пуст или нет видимых строк), выходим
    Если МассивСсылокОсновногоДокумента.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    // 2. Выполняем дополнительный запрос для получения связанных данных
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ВТ_СвязанныеДокументы.ДокументИсточник КАК Ссылка,
        |    МАКСИМУМ(ВТ_СвязанныеДокументы.НомерПодчиненногоДокумента) КАК ПервыйПодчиненныйДокументНомер
        |ИЗ
        |    (ВЫБРАТЬ РАЗЛИЧНЫЕ
        |        м12_СвязанныеДокументы.ДокументИсточник,
        |        м12_СвязанныеДокументы.ПодчиненныйДокумент.Номер КАК НомерПодчиненногоДокумента,
        |        м12_СвязанныеДокументы.ПодчиненныйДокумент.Ссылка КАК ПодчиненныйДокументСсылка
        |    ИЗ
        |        РегистрСведений.м12_СвязанныеДокументы КАК м12_СвязанныеДокументы
        |    ГДЕ
        |        м12_СвязанныеДокументы.ДокументИсточник В (&МассивСсылокОсновногоДокумента)
        |        И м12_СвязанныеДокументы.ДокументИсточник ССЫЛКА Документ.м12_ППЗ
        |        И м12_СвязанныеДокументы.ПодчиненныйДокумент ССЫЛКА Документ.э_ПозицияПЗ) КАК ВТ_СвязанныеДокументы
        |СГРУППИРОВАТЬ ПО
        |    ВТ_СвязанныеДокументы.ДокументИсточник"; // Группируем, чтобы получить одну строку на каждый основной документ

    Запрос.УстановитьПараметр("МассивСсылокОсновногоДокумента", МассивСсылокОсновногоДокумента);

    Выборка = Запрос.Выполнить().Выбрать();

    // 3. Заполняем "пустые" поля динамического списка полученными данными
    Пока Выборка.Следующий() Цикл
        // Получаем объект "СтрокаСписка" для текущей ссылки
        СтрокаСписка = СоответствиеСтрок.Получить(Выборка.Ссылка);

        Если СтрокаСписка <> Неопределено Тогда
            // Заполняем созданное ранее поле "НомераПодчиненныхДокументов"
            // В данном примере мы берем только первый номер, используя МАКСИМУМ
            СтрокаСписка.Данные["НомераПодчиненныхДокументов"] = "Детализированная ППЗ №" + Выборка.ПервыйПодчиненныйДокументНомер;
        КонецЕсли;
    КонецЦикла;

КонецПроцедуры

Рассмотрим ключевые моменты в этой процедуре:

  1. Получение массива ссылок:

    Параметр Строки, передаваемый в процедуру ПриПолученииДанныхНаСервере, содержит коллекцию объектов СтрокаСписка для тех элементов списка, которые в данный момент видимы пользователю на экране. Для каждого элемента мы можем получить его ключевую ссылку:

    
    МассивСсылокОсновногоДокумента = Новый Массив;
    СоответствиеСтрок = Новый Соответствие;
    Для Каждого КлючИЗначение Из Строки Цикл
        СсылкаНаДокумент = КлючИЗначение.Ключ.Ссылка;
        МассивСсылокОсновногоДокумента.Добавить(СсылкаНаДокумент);
        СоответствиеСтрок.Вставить(СсылкаНаДокумент, КлючИЗначение.Значение);
    КонецЦикла;
    

    Метод Строки.ПолучитьКлючи также возвращает массив ссылок, но он формируется для всего динамического списка и содержит только ссылки основного типа. В приведенном примере мы явно перебираем коллекцию Строки, что дает нам не только ссылки, но и возможность создать удобное соответствие СоответствиеСтрок для быстрого доступа к объекту СтрокаСписка по его ссылке.

    Важно: В массив МассивСсылокОсновногоДокумента попадут ссылки только на основной объект динамического списка (в нашем случае это Документ.м12_ППЗ), даже если в запросе ДС есть другие ссылочные поля (например, ссылка на справочник). Это позволяет максимально точно сформировать запрос к связанным данным.

  2. Создание и выполнение дополнительного запроса:

    Внутри обработчика мы создаем новый запрос. Критически важно использовать сформированный МассивСсылокОсновногоДокумента в условии ГДЕ ... В (&МассивСсылокОсновногоДокумента). Это гарантирует, что запрос будет выполняться только для небольшого количества (обычно 20-30) отображаемых строк, а не для всех документов в базе, что обеспечивает высокую производительность.

    В этом запросе мы обращаемся к регистру сведений "м12_СвязанныеДокументы". Поскольку нам нужно получить агрегированные данные (например, номера всех подчиненных документов, сведенные в одну строку, или только номер первого/последнего), мы используем группировку по ДокументИсточник.

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

    Пример запроса, приведенный в сообщении 16, был излишне сложным и неоптимизированным, так как временные таблицы формировались без учета параметра &ДинамическийСписокППЗ в самом начале. Это привело бы к тому, что СУБД сначала выбирала бы данные по всем документам, а уже потом фильтровала их на последнем шаге. Правильный подход — максимально сужать область выборки с самого начала.

  3. Заполнение данных в динамическом списке:

    После выполнения дополнительного запроса, мы перебираем его результат (Выборка). Для каждой строки выборки, содержащей ссылку на основной документ и связанные данные, мы находим соответствующий объект СтрокаСписка из динамического списка, используя ранее созданное СоответствиеСтрок.

    
    СтрокаСписка = СоответствиеСтрок.Получить(Выборка.Ссылка);
    Если СтрокаСписка <> Неопределено Тогда
        СтрокаСписка.Данные["НомераПодчиненныхДокументов"] = "Детализированная ППЗ №" + Выборка.ПервыйПодчиненныйДокументНомер;
    КонецЕсли;
    

    Важно обращаться к данным строки списка через свойство Данные и указывать имя поля в квадратных скобках (например, СтрокаСписка.Данные["ИмяПоля"]). Это гарантирует, что данные будут корректно присвоены полю, которое было создано как "заглушка" в основном запросе динамического списка.

Отображение нескольких подчиненных документов в одной колонке

Если требуется отобразить не только "первый" номер подчиненного документа, а собрать в одной колонке список из всех связанных номеров, разделенных разделителем, вы можете модифицировать запрос или логику в цикле Пока Выборка.Следующий(). В рамках запроса 1С нет стандартной функции для строковой конкатенации агрегатов (как STRING_AGG в SQL Server), поэтому часто приходится делать это вручную на стороне сервера:

  1. Сбор номеров в цикле: Выполните запрос, который выбирает все номера для каждого основного документа без группировки (или с временной таблицей, которая уже содержит все номера). Затем в цикле по результатам запроса соберите номера в строку, используя объект Строка или Массив.
  2. 
    // Пример модифицированного запроса для сбора всех номеров:
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    м12_СвязанныеДокументы.ДокументИсточник КАК Ссылка,
        |    м12_СвязанныеДокументы.ПодчиненныйДокумент.Номер КАК НомерПодчиненногоДокумента
        |ИЗ
        |    РегистрСведений.м12_СвязанныеДокументы КАК м12_СвязанныеДокументы
        |ГДЕ
        |    м12_СвязанныеДокументы.ДокументИсточник В (&МассивСсылокОсновногоДокумента)
        |    И м12_СвязанныеДокументы.ДокументИсточник ССЫЛКА Документ.м12_ППЗ
        |    И м12_СвязанныеДокументы.ПодчиненныйДокумент ССЫЛКА Документ.э_ПозицияПЗ
        |УПОРЯДОЧИТЬ ПО
        |    м12_СвязанныеДокументы.ДокументИсточник,
        |    м12_СвязанныеДокументы.ПодчиненныйДокумент.Номер"; // Важно для консистентного порядка
    
    // ... выполнение запроса ...
    
    // Дополнительная структура для сбора номеров по каждому основному документу
    СоответствиеНомеровПодчиненных = Новый Соответствие;
    
    Пока Выборка.Следующий() Цикл
        СсылкаОсновного = Выборка.Ссылка;
        Если СоответствиеНомеровПодчиненных.Получить(СсылкаОсновного) = Неопределено Тогда
            СоответствиеНомеровПодчиненных.Вставить(СсылкаОсновного, Новый Массив);
        КонецЕсли;
        СоответствиеНомеровПодчиненных.Получить(СсылкаОсновного).Добавить("Детализированная ППЗ №" + Выборка.НомерПодчиненногоДокумента);
    КонецЦикла;
    
    // После сбора всех номеров, заполняем строки списка
    Для Каждого КлючИЗначение Из СоответствиеНомеровПодчиненных Цикл
        СсылкаОсновного = КлючИЗначение.Ключ;
        МассивНомеров = КлючИЗначение.Значение;
        СтрокаСписка = СоответствиеСтрок.Получить(СсылкаОсновного);
    
        Если СтрокаСписка <> Неопределено Тогда
            СтрокаСписка.Данные["НомераПодчиненныхДокументов"] = СтрСоединить(МассивНомеров, ", ");
        КонецЕсли;
    КонецЦикла;
    

    Обратите внимание, что для корректной работы этого подхода запрос должен быть упорядочен сначала по ДокументИсточник, а затем по полю, которое вы хотите конкатенировать (например, номеру подчиненного документа), чтобы обеспечить предсказуемый порядок объединения. Использование дополнительного Соответствия для временного хранения собранных номеров позволяет эффективно собрать все данные, а затем одним проходом заполнить строки динамического списка.

Таким образом, используя обработчик ПриПолученииДанныхНаСервере, мы можем эффективно решить задачу отображения связанных данных из отношений "многие ко многим" в динамическом списке 1С, поддерживая высокую производительность и гибкость.

← На главную