При разработке интерфейсов в конфигурациях 1С часто возникает задача отображения агрегированных данных из табличных частей документов непосредственно в форме списка. Например, когда для каждого документа в списке необходимо сразу видеть перечень номеров вагонов, номенклатуру или итоговый вес без необходимости открывать сам документ. В этой статье мы подробно разберем, как решить эту задачу на обычных формах, проанализируем проблемы производительности и рассмотрим два основных метода реализации с практическими примерами кода.
Представим, что у нас есть документ Сертификат с табличной частью Вагоны. В этой табличной части присутствуют реквизиты НомерВагона (тип Строка) и Вес (тип Число). Нам требуется вывести на форму списка две колонки: список номеров вагонов через запятую и суммарный вес по документу.
Для реализации этой задачи в обычных формах у разработчика есть два принципиально разных пути:
Давайте подробно разберем каждый из этих способов, их преимущества, недостатки и особенности реализации.
Этот способ является наиболее простым с точки зрения кода формы списка. Мы добавляем в сам документ Сертификат два новых реквизита шапки: ВесПартии (Число) и СписокНомеровВагонов (Строка неограниченной длины или фиксированной, если мы уверены в лимитах).
Главное правило при таком подходе: заполнение реквизитов шапки должно происходить строго на сервере при записи объекта, чтобы данные всегда оставались актуальными. Нам ни в коем случае нельзя писать этот код только в модуле формы, так как при программном изменении документов из внешних обработок или при обмене данными обработчики формы не сработают, и данные в списке станут неактуальными. Разместим логику в модуле объекта документа.
Разберем по шагам правильную реализацию в процедуре ПередЗаписью модуля объекта:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Если запись инициирована из обмена данными с признаком Загрузка,
// обычно расчеты отключают, но в данном случае нам важно сохранить актуальность
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// 1. Рассчитываем итоговый вес по табличной части
ВесПартии = Вагоны.Итог("Вес");
// 2. Формируем строку номеров вагонов без дублирования кода и лишних пробелов
МассивНомеров = Новый Массив;
Для Каждого Стр Из Вагоны Цикл
НомерОчищенный = СокрЛП(Стр.НомерВагона);
Если ЗначениеЗаполнено(НомерОчищенный) Тогда
МассивНомеров.Добавить(НомерОчищенный);
КонецЕсли;
КонецЦикла;
// Склеиваем массив в строку с учетом версии платформы
#Если Клиент Или НаСервере Тогда
// Для платформы 8.3.6 и выше используем встроенный метод
СписокНомеровВагонов = СтрСоединить(МассивНомеров, ", ");
#Иначе
// Для старых версий платформ или при работе в специфическом окружении
// можно использовать ручной обход или функции БСП
СтрокаРезультат = "";
Для Каждого Номер Из МассивНомеров Цикл
СтрокаРезультат = СтрокаРезультат + ?(СтрокаРезультат = "", "", ", ") + Номер;
КонецЦикла;
СписокНомеровВагонов = СтрокаРезультат;
#КонецЕсли;
КонецПроцедуры
Преимущества метода:
Недостатки метода:
Если мы не хотим менять структуру метаданных, добавлять новые реквизиты шапки и перезаписывать старые документы, мы можем получать данные динамически. На форумах часто предлагают использовать событие ПриВыводеСтроки. Однако этот подход крайне неоптимален!
Выясним причину: событие ПриВыводеСтроки срабатывает отдельно для каждой отображаемой строки. Если на экране выводится 50 строк, система выполнит 50 отдельных запросов к базе данных, что приведет к катастрофическому падению производительности («проблема N+1 запроса»).
Правильным решением для обычных форм является использование события ПриПолученииДанных табличного поля. В этот обработчик передается коллекция ОформленияСтрок, содержащая пакет строк, которые в данный момент реально отображаются на экране пользователя (обычно не более 20–30 строк). Мы можем собрать все ссылки этих документов и получить данные по их табличным частям одним-единственным запросом.
Разберем пример реализации этого обработчика в модуле формы списка документов:
Процедура ДокументСписокПриПолученииДанных(Элемент, ОформленияСтрок)
// Шаг 1. Собираем массив ссылок на отображаемые документы
МассивСсылок = Новый Массив;
СоответствиеСтрок = Новый Соответствие; // Для сопоставления оформления строки с конкретным документом
Для Каждого ОформлениеСтроки Из ОформленияСтрок Цикл
ДанныеСтроки = ОформлениеСтроки.ДанныеСтроки;
Если ДанныеСтроки <> Неопределено И ДанныеСтроки.Ссылка <> Неопределено Тогда
МассивСсылок.Добавить(ДанныеСтроки.Ссылка);
СоответствиеСтрок.Вставить(ДанныеСтроки.Ссылка, ОформлениеСтроки);
КонецЕсли;
КонецЦикла;
Если МассивСсылок.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Шаг 2. Запрашиваем данные табличной части по всем документам пакета одним запросом
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СертификатВагоны.Ссылка КАК Ссылка,
| СертификатВагоны.НомерВагона КАК НомерВагона,
| СертификатВагоны.Вес КАК Вес
|ИЗ
| Документ.Сертификат.Вагоны КАК СертификатВагоны
|ГДЕ
| СертификатВагоны.Ссылка В(&МассивСсылок)
|ИТОГИ
| СУММА(Вес)
|ПО
| Ссылка";
Запрос.УстановитьПараметр("МассивСсылок", МассивСсылок);
РезультатЗапроса = Запрос.Выполнить();
// Шаг 3. Обходим результаты и распределяем данные по ячейкам оформления
ВыборкаДокументов = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокументов.Следующий() Цикл
МассивНомеровТекущегоДокумента = Новый Массив;
// Перебираем детальные записи (строки вагонов) для текущего документа
ВыборкаДетали = ВыборкаДокументов.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
НомерВагона = СокрЛП(ВыборкаДетали.НомерВагона);
Если ЗначениеЗаполнено(НомерВагона) Тогда
МассивНомеровТекущегоДокумента.Добавить(НомерВагона);
КонецЕсли;
КонецЦикла;
// Объединяем номера вагонов в единую строку
СтрокаВагонов = СтрСоединить(МассивНомеровТекущегоДокумента, ", ");
СуммарныйВес = ВыборкаДокументов.Вес;
// Находим сохраненную строку оформления по ссылке документа
ОформлениеСтроки = СоответствиеСтрок.Получить(ВыборкаДокументов.Ссылка);
Если ОформлениеСтроки <> Неопределено Тогда
// Записываем подготовленный текст в добавленные колонки формы списка
ОформлениеСтроки.Ячейки.КолонкаНомераВагонов.УстановитьТекст(СтрокаВагонов);
ОформлениеСтроки.Ячейки.КолонкаВес.УстановитьТекст(Формат(СуммарныйВес, "ЧЦ=15; ЧДЦ=3"));
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Преимущества метода:
Недостатки метода:
Для того чтобы сделать правильный выбор между хранением данных в шапке и динамическим запросом в событии ПриПолученииДанных, проанализируем следующие факторы:
Если для пользователей критически важна возможность быстрого поиска конкретного вагона непосредственно через строку поиска списка документов или сортировка по весу партии, то вашим выбором должен стать Способ 1 (Денормализация). Наличие реальных полей в базе данных позволит СУБД быстро строить индексы и осуществлять мгновенный поиск.
Если же форма списка носит исключительно информационный характер, а поиск документов выполняется по дате, контрагенту или номеру самого сертификата, то наиболее изящным и технологичным решением будет Способ 2 (ПриПолученииДанных). Он сэкономит дисковое пространство и избавит вас от написания разовых обработок по заполнению исторических данных.