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