При разработке отчетов в системе компоновки данных (СКД) на платформе 1С программисты часто сталкиваются с необходимостью вывести иерархическую структуру (дерево). Классический пример — разузлование спецификаций номенклатуры, вывод структуры подчиненности организации или отображение каталога товаров с неограниченной вложенностью. Если данные выбираются прямым запросом из базы данных, то настроить иерархию в СКД достаточно просто. Но что делать, если исходные данные собираются программно в сложный массив и возвращаются в виде Таблицы значений (ТЗ)?
Существует распространенное заблуждение, что СКД не умеет строить рекурсивные связи «сама на себя» для внешних наборов данных (наборов данных типа «Объект»). На самом деле компоновщик отлично справляется с этой задачей непосредственно в оперативной памяти. В этой статье мы подробно разберем, как правильно настроить вывод полноценного дерева из таблицы значений штатными средствами СКД, а также решим проблему дублирования элементов с помощью синтетических ключей.
Разработчики часто пытаются настроить рекурсивную связь наборов данных по аналогии с наборами данных типа «Запрос». В запросах для построения иерархии используется конструкция вида ГДЕ Ссылка В (&Родители), а в связях наборов указывается соответствующий параметр. Пытаясь перенести эту логику на таблицы значений, программисты сталкиваются с тем, что в наборе данных — объекте некуда передать этот параметр, и начинают изобретать сложные обходные пути (например, программную модификацию макета компоновки или ручную отрисовку дерева в табличном документе).
Как обстоит дело на самом деле: СКД умеет выполнять рекурсивное сопоставление строк («иерархический спуск») в оперативной памяти без использования каких-либо параметров связи или дополнительных запросов к БД. Достаточно просто правильно описать поля связи в интерфейсе конструктора настроек схемы компоновки данных.
Предположим, у нас есть программно сформированная таблица значений, которая содержит информацию о деталях и узлах изделия. Каждая строка содержит ссылку на саму себя через поле родителя. Давайте подготовим структуру нашей ТЗ:
// Создаем структуру таблицы значений
ТаблицаДанных = Новый ТаблицаЗначений;
ТаблицаДанных.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаДанных.Колонки.Добавить("РодительНоменклатуры", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаДанных.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
ТаблицаДанных.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
При заполнении этой таблицы корневые элементы (самый верхний уровень дерева) будут иметь значение Справочник.Номенклатура.ПустаяСсылка() (или Неопределено / NULL) в колонке РодительНоменклатуры. Подчиненные элементы будут ссылаться на своего непосредственного родителя.
Для того чтобы СКД поняла структуру нашего дерева, выполним следующие шаги в конструкторе схемы компоновки:
НаборДанных1. В поле «Имя объекта содержащего данные» укажем имя нашей ТЗ, например, ТаблицаДанных.Номенклатура, РодительНоменклатуры, Количество, Цена.НаборДанных1НаборДанных1 (указываем этот же набор данных!)НоменклатураРодительНоменклатурыЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка).Номенклатура, нажмем на кнопку добавления группировки и выберем пункт «Новая иерархия...» (или выберем тип группировки Иерархия в свойствах уже созданной группировки). В качестве поля группировки укажем Номенклатура.При программном выводе отчета достаточно передать подготовленную ТЗ в процессор компоновки:
ВнешниеНаборыДанных = Новый Структура;
ВнешниеНаборыДанных.Вставить("ТаблицаДанных", ТаблицаДанных);
// Стандартный вывод отчета с использованием внешнего набора данных
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, КомпоновщикНастроек.ПолучитьНастройки());
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборыДанных);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(Результат);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
На практике при решении задач разузлования (например, когда мы раскладываем спецификацию готовой продукции на материалы и полуфабрикаты) возникает серьезная проблема: один и тот же материал может входить в состав разных узлов на разных уровнях иерархии. Например, деталь «Болт» входит в узел «Рычаг», и этот же «Болт» входит в узел «Лапа».
Если мы настроим рекурсивную связь по полю Номенклатура, СКД не сможет корректно отобразить дерево. Возникнет логический конфликт: СКД попытается подчинить «Болт» одновременно и «Рычагу», и «Лапе», что приведет либо к «схлопыванию» ветвей дерева, либо к зацикливанию и бесконечной рекурсии.
Для решения этой проблемы мы должны использовать синтетические (технические) ключи связи вместо реальных ссылок номенклатуры. Давайте модернизируем нашу таблицу значений, добавив в нее служебные числовые колонки:
ТаблицаДанных = Новый ТаблицаЗначений;
// Бизнес-данные
ТаблицаДанных.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаДанных.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
ТаблицаДанных.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
// Синтетические ключи для связи в СКД
ТаблицаДанных.Колонки.Добавить("ИДСтроки", Новый ОписаниеТипов("Число"));
ТаблицаДанных.Колонки.Добавить("ИДРодителя", Новый ОписаниеТипов("Число"));
При программном заполнении таблицы мы гарантируем уникальность поля ИДСтроки для каждой строки таблицы (например, используя простой автоинкрементный счетчик: 1, 2, 3 и т.д.). В поле ИДРодителя мы записываем значение ИДСтроки той строки, которая является родителем для текущей записи. У самых верхних (корневых) строк в поле ИДРодителя записываем значение 0.
Теперь перенастроим связи наборов данных в СКД на использование этих ключей:
НаборДанных1НаборДанных1ИДСтрокиИДРодителя0В настройках отчета группировку теперь нужно делать не по полю Номенклатура, а по уникальному полю ИДСтроки с типом группировки Иерархия. Чтобы отчет выглядел красиво, в выбранные поля группировки мы добавляем поле Номенклатура, а само техническое поле ИДСтроки можем скрыть из вывода или убрать его отображение. Этот элегантный прием позволяет строить абсолютно любые, сколь угодно сложные и разветвленные деревья, полностью исключая зацикливание и неверное группирование данных.
Иногда передавать всю структуру в одной перегруженной таблице неудобно или нерационально с точки зрения объема передаваемых данных. В таких случаях мы можем применить архитектурный паттерн разделения данных и иерархии, передав в СКД два разных набора данных типа «Объект»:
НаборДанныхДанные — содержит плоские строки с показателями (Номенклатура, Количество, Цена).НаборДанныхИерархия — содержит только структуру подчиненности (Номенклатура, РодительНоменклатуры).Связи настраиваются в два этапа:
НаборДанныхДанныеНаборДанныхИерархияНоменклатураНоменклатураНаборДанныхИерархияНаборДанныхИерархияРодительНоменклатурыНоменклатураЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)Такой подход делает схему чище и позволяет повторно использовать таблицу связей для разных видов отчетов.
Важно помнить, как СКД рассчитывает итоги по ресурсам (например, полям Количество или Сумма) при построении дерева. Если ваша исходная таблица значений уже содержит готовые предрассчитанные агрегированные данные по узлам и родительским уровням, стандартное суммирование СКД (с использованием функции Сумма(ИмяПоля) на вкладке «Ресурсы») приведет к катастрофическому завышению итогов. Дочерние элементы приплюсуются к уже заполненным родительским значениям.
Варианты решения этой проблемы:
Максимум(...) или Среднее(...) вместо Сумма(...). Это позволит вывести исходные цифры "как есть", без повторного сложения.Сумма(...) отработает корректно и автоматически соберет итоги снизу вверх по дереву.Используя эти проверенные подходы, мы можем быстро, гибко и без написания громоздкого программного кода выводить сложные иерархические структуры в 1С, полностью контролируя процесс компоновки данных.