В практике разработчика 1С часто возникает задача преобразования «плоского» списка данных из таблицы значений в иерархическую структуру — дерево значений. Это может потребоваться при загрузке данных из внешних файлов (Excel, JSON, CSV), выводе сложных составов изделий в ERP или при визуализации данных, где иерархия заложена в виде кодов или уровней — для этого подойдёт обработка построения иерархических печатных форм. Разберем подробно, как решить эту задачу различными способами, от классической рекурсии до использования встроенных механизмов платформы.
Это классический и наиболее универсальный способ. Он применяется, когда в каждой строке таблицы значений (ТЗ) есть уникальный идентификатор (например, ID) и идентификатор родителя (ParentID). Проанализируем алгоритм: мы ищем строки верхнего уровня (где родитель пуст), добавляем их в корень дерева, а затем для каждой добавленной строки вызываем ту же функцию, чтобы найти её «детей».
Рассмотрим пример реализации такой функции:
Функция ВыгрузитьТаблицуЗначенийВДеревоЗначений(Таблица, КлючСтроки = "Идентификатор", КлючСвязи = "Родитель") Экспорт
Дерево = Новый ДеревоЗначений;
// Переносим колонки из таблицы в дерево
Для Каждого Колонка Из Таблица.Колонки Цикл
Дерево.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения);
КонецЦикла;
// Вызываем вспомогательную рекурсивную процедуру для заполнения
ЗаполнитьУровеньДерева(Таблица, Дерево.Строки, Неопределено, КлючСтроки, КлючСвязи);
Возврат Дерево;
КонецФункции
Процедура ЗаполнитьУровеньДерева(Таблица, СтрокиДерева, ЗначениеРодителя, КлючСтроки, КлючСвязи)
// Отбираем строки, принадлежащие текущему родителю
ПараметрыОтбора = Новый Структура(КлючСвязи, ЗначениеРодителя);
НайденныеСтроки = Таблица.НайтиСтроки(ПараметрыОтбора);
Для Каждого СтрокаТЗ Из НайденныеСтроки Цикл
НоваяСтрока = СтрокиДерева.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЗ);
// Рекурсивный вызов для поиска подчиненных строк
ЗаполнитьУровеньДерева(Таблица, НоваяСтрока.Строки, СтрокаТЗ[КлючСтроки], КлючСтроки, КлючСвязи);
КонецЦикла;
КонецПроцедуры
Важный нюанс: использование метода НайтиСтроки внутри рекурсии на больших объемах данных может привести к замедлению работы. Проанализируем ситуацию: если в таблице 10 000 строк, система будет многократно сканировать её всю. Для оптимизации можно предварительно отсортировать таблицу или использовать индексацию, если это позволяют механизмы платформы.
Иногда данные приходят в линейном виде, где нет явной ссылки на родителя, но указан номер уровня (1, 2, 3...). Например, так часто строится дерево состава изделия в 1С:ERP. В этом случае рекурсия не совсем удобна, так как родителем текущей строки является последняя созданная строка предыдущего уровня.
Разберем, как реализовать заполнение такого дерева через цикл и массив текущих уровней:
// Предположим, в ТЗ есть колонка "Уровень" (число)
Процедура СформироватьДеревоПоУровням(ТЗ, Дерево)
// Массив для хранения ссылок на текущие родительские строки по уровням
// Индекс массива соответствует номеру уровня
РодителиПоУровням = Новый Соответствие;
Для Каждого СтрокаТЗ Из ТЗ Цикл
ТекущийУровень = СтрокаТЗ.Уровень;
Если ТекущийУровень = 1 Тогда
// Строки первого уровня добавляем в корень
НоваяСтрока = Дерево.Строки.Добавить();
Иначе
// Ищем родителя на уровень выше
РодительскаяСтрока = РодителиПоУровням.Получить(ТекущийУровень - 1);
Если РодительскаяСтрока <> Неопределено Тогда
НоваяСтрока = РодительскаяСтрока.Строки.Добавить();
Иначе
// Если родитель не найден (нарушена логика данных), добавляем в корень
НоваяСтрока = Дерево.Строки.Добавить();
КонецЕсли;
КонецЕсли;
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЗ);
// Запоминаем текущую строку как потенциального родителя для следующего уровня
РодителиПоУровням.Вставить(ТекущийУровень, НоваяСтрока);
КонецЦикла;
КонецПроцедуры
Этот метод работает за один проход по таблице значений (линейная сложность), что делает его крайне эффективным для больших структур.
Многие разработчики забывают, что мощный движок 1С — СКД — умеет строить иерархию автоматически — для этого есть консоль кода, запросов и СКД для разработчика. Если ваша таблица содержит ссылки на объекты, имеющие иерархию в метаданных, или если вы можете задать связь «сама на себя», то СКД сделает всё за вас.
Посмотрим на последовательность действий:
ДеревоЗначений.Этот способ удобен тем, что платформа берет на себя все вычисления и оптимизацию обхода дерева.
Если данные уже находятся на сервере, мы можем поместить таблицу значений во временную таблицу и выполнить запрос с предложением ИТОГИ ПО ... ИЕРАРХИЯ. Выясним причину, почему это удобно: результат запроса можно выгрузить в дерево одной командой РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам).
Пример кода:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_Данные ИЗ &ТЗ КАК ТЗ;
|ВЫБРАТЬ * ИЗ ВТ_Данные
|ИТОГИ ПО Ссылка ИЕРАРХИЯ";
Запрос.УстановитьПараметр("ТЗ", МояТаблица);
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамИерархия);
Важно: этот метод работает корректно только если иерархия строится по ссылочным типам (например, справочник Номенклатура), у которых в метаданных определен владелец или родитель.
Проанализируем основные моменты, которые помогут сделать работу с деревьями более стабильной:
ERP (например, разузлование спецификации) всегда отдавайте предпочтение циклам с индексацией (Соответствие) вместо рекурсивного НайтиСтроки — для этого пригодится анализ себестоимости продукции с разузлованием.ДеревоЗначений доступен только на сервере. Если вам нужно отобразить дерево на клиенте в управляемой форме, используйте ДанныеФормыДерево и метод ЗначениеВРеквизитФормы — есть готовая динамическое дерево остатков для управляемых форм.ПриРазворачивании.Таким образом, выбор метода зависит от структуры вашей исходной таблицы. Если есть «Родитель» — используйте рекурсию или запросы. Если есть только «Уровень» — используйте линейный обход с фиксацией текущего родителя в массиве или соответствии.