Из дерева в плоский список: рекурсивный обход строк и сохранение иерархии
Содержание
Готового метода, который выгрузит дерево значений в таблицу значений одной строкой, в платформе 1С 8.3 нет. Алгоритм такой: создать таблицу с теми же колонками, что у дерева, и обойти все строки рекурсивно, копируя каждую через ЗаполнитьЗначенияСвойств().
Сложности начинаются потом. Теряется иерархия, задваиваются суммы групп, на форме вместо дерева оказывается ДанныеФормыДерево. Ниже рабочий код и разбор каждой из этих ловушек. Код серверный и работает одинаково в любой конфигурации: это механизм платформы, а не БП или УТ.
Чем дерево отличается от таблицы и что теряется при выгрузке
У таблицы значений все строки лежат в одной коллекции Таблица. У дерева строки верхнего уровня лежат в Дерево.Строки, а у каждой строки есть своя коллекция Строка.Строки с подчинёнными. Глубина не ограничена.
Что есть у строки дерева и чего нет у строки таблицы:
- 🌳
Строка.Родитель: ссылка на строку уровнем выше, у верхнего уровняНеопределено; - 📂
Строка.Строки: коллекция дочерних строк; - 🔢
Строка.Уровень(): метод, возвращает глубину, верхний уровень равен 0; - 📍 положение среди соседей:
Строка.Родитель.Строки.Индекс(Строка).
При выгрузке в плоскую таблицу всё это пропадает. Если иерархия потом не нужна, это не проблема. Если нужна, например для отчёта с отступами или для обратной сборки дерева, её надо явно записать в служебные колонки.
| Что нужно на выходе | Какие колонки добавить |
|---|---|
| Просто все строки подряд | ничего, только колонки дерева |
| Отступы в печатной форме или выгрузке в Excel | Уровень |
| Возможность собрать дерево обратно | НомерСтроки и НомерРодителя |
| Только конечные записи без групп | ничего, фильтр «нет подчинённых строк» |
| Путь вида 1.3.2 для сортировки | Путь (строка) |
Готовим таблицу значений: копируем колонки дерева
Колонки дерева и таблицы описываются одинаково: имя, тип, заголовок, ширина. Поэтому копирование сводится к циклу по Дерево.Колонки.
Функция НоваяТаблицаПоДереву(Дерево)
Таблица = Новый ТаблицаЗначений;
Для Каждого Колонка Из Дерево.Колонки Цикл
Таблица.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения,
Колонка.Заголовок, Колонка.Ширина);
КонецЦикла;
Возврат Таблица;
КонецФункции
Тип колонки переносите обязательно. Колонка без типа принимает что угодно, но при выводе в табличный документ, в построитель или в запрос через ПоместитьВоВременнуюТаблицу начнутся проблемы. Запрос не примет колонку без описания типов.
Служебные колонки добавляйте с явными типами:
ТипЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(10, 0));
ТипСтрока = Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(100));
Таблица.Колонки.Добавить("Уровень_", ТипЧисло, "Уровень");
Таблица.Колонки.Добавить("НомерСтроки_", ТипЧисло);
Таблица.Колонки.Добавить("НомерРодителя_", ТипЧисло);
Не называйте служебные колонки Родитель, Строки или Уровень без суффикса. Во-первых, такая колонка может уже быть в дереве, и Колонки.Добавить() упадёт на повторном имени. Во-вторых, у строки дерева есть одноимённые свойства, и при копировании через ЗаполнитьЗначенияСвойств() в колонку попадёт не то, что вы ждёте.
Имена с подчёркиванием в конце (Уровень_) с колонками прикладных деревьев почти не пересекаются. Ещё надёжнее заранее проверить имя через Дерево.Колонки.Найти("Уровень_"): метод вернёт Неопределено, если колонки нет.
Рекурсивная выгрузка дерева значений в таблицу
Базовый вариант: процедура принимает коллекцию строк, копирует каждую строку и вызывает сама себя для её подчинённых строк.
Процедура ВыгрузитьСтрокиДерева(СтрокиДерева, Таблица, СписокКолонок)
Для Каждого СтрокаДерева Из СтрокиДерева Цикл
НоваяСтрока = Таблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева, СписокКолонок);
ВыгрузитьСтрокиДерева(СтрокаДерева.Строки, Таблица, СписокКолонок);
КонецЦикла;
КонецПроцедуры
Функция ДеревоВТаблицу(Дерево) Экспорт
Таблица = НоваяТаблицаПоДереву(Дерево);
ИменаКолонок = Новый Массив;
Для Каждого Колонка Из Дерево.Колонки Цикл
ИменаКолонок.Добавить(Колонка.Имя);
КонецЦикла;
СписокКолонок = СтрСоединить(ИменаКолонок, ",");
ВыгрузитьСтрокиДерева(Дерево.Строки, Таблица, СписокКолонок);
Возврат Таблица;
КонецФункции
Порядок строк в результате — обход в глубину: группа, сразу за ней её содержимое, потом следующая группа. Именно так дерево выглядит на экране в развёрнутом виде, поэтому таблицу можно сразу выводить на печать.
Третий параметр ЗаполнитьЗначенияСвойств() здесь не для красоты. Он ограничивает копирование колонками дерева и исключает служебные свойства строки. Список собирается один раз до обхода, а не на каждой строке.
💡 Если нужны только конечные строки (листья), добавьте условие перед Таблица.Добавить(): Если СтрокаДерева.Строки.Количество() = 0 Тогда. Рекурсивный вызов оставьте вне условия, иначе обход не дойдёт до нижних уровней.
Как сохранить уровни иерархии, родителя и отступы
Расширим процедуру: передаём номер строки-родителя и счётчик строк. Номер строки берём из Таблица.Количество() сразу после добавления: он совпадает с порядковым номером новой строки.
Процедура ВыгрузитьСИерархией(СтрокиДерева, Таблица, СписокКолонок,
НомерРодителя, Уровень)
Для Каждого СтрокаДерева Из СтрокиДерева Цикл
НоваяСтрока = Таблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева, СписокКолонок);
НоваяСтрока.НомерСтроки_ = Таблица.Количество();
НоваяСтрока.НомерРодителя_ = НомерРодителя;
НоваяСтрока.Уровень_ = Уровень;
ВыгрузитьСИерархией(СтрокаДерева.Строки, Таблица, СписокКолонок,
НоваяСтрока.НомерСтроки_, Уровень + 1);
КонецЦикла;
КонецПроцедуры
// Первый вызов: родителя нет, уровень 0
ВыгрузитьСИерархией(Дерево.Строки, Таблица, СписокКолонок, 0, 0);
У строк верхнего уровня НомерРодителя_ равен 0. По паре «номер — номер родителя» дерево собирается обратно, а в запросе по такой таблице можно отбирать содержимое конкретной группы.
Уровень можно не передавать параметром, а брать через СтрокаДерева.Уровень(). Результат тот же, но параметр дешевле: метод при каждом вызове поднимается по цепочке родителей.
Проверка: собираем дерево обратно
Лучший тест выгрузки — восстановить из таблицы исходное дерево и сравнить количество строк. Родитель в таблице всегда стоит выше потомка, поэтому хватает одного прохода:
Функция ТаблицаВДерево(Таблица, Дерево)
Созданные = Новый Соответствие;
Для Каждого СтрокаТаблицы Из Таблица Цикл
Если СтрокаТаблицы.НомерРодителя_ = 0 Тогда
Коллекция = Дерево.Строки;
Иначе
Коллекция = Созданные[СтрокаТаблицы.НомерРодителя_].Строки;
КонецЕсли;
НоваяСтрока = Коллекция.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТаблицы);
Созданные.Вставить(СтрокаТаблицы.НомерСтроки_, НоваяСтрока);
КонецЦикла;
Возврат Дерево;
КонецФункции
Дерево-приёмник создайте заранее с теми же колонками, но без служебных. Тогда ЗаполнитьЗначенияСвойств() скопирует только совпадающие по имени колонки, а номера останутся в таблице.
Отступы для печати и Excel
В табличном документе отступ лучше делать свойством ячейки Отступ, а не пробелами. Если же результат уходит в CSV или в текст, отступ собирают в строку:
Функция СтрокаОтступа(Уровень)
Результат = "";
Для Сч = 1 По Уровень Цикл
Результат = Результат + " ";
КонецЦикла;
Возврат Результат;
КонецФункции
// при выгрузке
НоваяСтрока.Наименование = СтрокаОтступа(Уровень) + СтрокаДерева.Наименование;
Так значение колонки меняется. Если наименование потом будет участвовать в поиске или сравнении, храните отступ в отдельной колонке, а исходный текст не трогайте.
Путь строки для сортировки
Колонка Путь_ со значениями вида 1.3.2 позволяет восстановить порядок после любой сортировки таблицы. Номер строки среди соседей берут через Индекс() коллекции, а число переводят в строку только через Формат(), иначе разряды разделятся пробелом:
Номер = Формат(СтрокиДерева.Индекс(СтрокаДерева) + 1, "ЧГ=0");
НоваяСтрока.Путь_ = ?(ПутьРодителя = "", Номер, ПутьРодителя + "." + Номер);
Сортировать по такой строке напрямую нельзя: 1.10 встанет раньше 1.2. Для сортировки дополняйте номера нулями слева до одинаковой длины, например формат "ЧЦ=4; ЧВН=; ЧГ=0" даст 0002.
Выгрузка без рекурсии и скорость на больших деревьях
Рекурсия наглядна, но на очень глубоком дереве упирается в стек вызовов. Деревья из запросов редко глубже 5–10 уровней, а вот деревья, построенные по иерархическим справочникам или спецификациям, бывают глубже. Тогда обход переводят на явный стек из массива.
Функция ДеревоВТаблицуБезРекурсии(Дерево, СписокКолонок, Таблица)
Стек = Новый Массив;
// кладём строки верхнего уровня в обратном порядке
Количество = Дерево.Строки.Количество();
Для Сч = 1 По Количество Цикл
Стек.Добавить(Дерево.Строки[Количество - Сч]);
КонецЦикла;
Пока Стек.Количество() > 0 Цикл
Последний = Стек.ВГраница();
СтрокаДерева = Стек[Последний];
Стек.Удалить(Последний);
НоваяСтрока = Таблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева, СписокКолонок);
Подчиненные = СтрокаДерева.Строки;
Количество = Подчиненные.Количество();
Для Сч = 1 По Количество Цикл
Стек.Добавить(Подчиненные[Количество - Сч]);
КонецЦикла;
КонецЦикла;
Возврат Таблица;
КонецФункции
Дочерние строки кладутся в стек с конца, поэтому снимаются в исходном порядке. Результат совпадает с рекурсивным вариантом строка в строку. Удаление с конца массива дешёвое: элементы не сдвигаются.
Что ещё ускоряет выгрузку на десятках тысяч строк:
- Копируйте строку одним
ЗаполнитьЗначенияСвойств()с готовым списком колонок, а не присваиванием по каждой колонке в цикле. - Не вызывайте внутри обхода
Дерево.Колонки.Найти(),Строка.Уровень()и прочее, что можно посчитать один раз. - Индекс по служебной колонке добавляйте после выгрузки:
Таблица.Индексы.Добавить("НомерРодителя_"). Индекс, созданный до заполнения, перестраивается на каждой новой строке. - Выполняйте код на сервере. Гонять дерево между сервером и клиентом ради выгрузки незачем.
Если дерево получено из запроса через Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам), самый быстрый путь — не обходить его вообще. Вызовите РезультатЗапроса.Выгрузить() без параметра: получится обычная таблица значений. Результат запроса можно выгружать несколько раз, перевыполнять запрос не нужно.
Сравнение способов:
| Способ | Когда подходит | Ограничения |
|---|---|---|
| Рекурсивная процедура | Любое дерево обычной глубины | Стек вызовов на очень глубоких деревьях |
| Обход через массив-стек | Глубокие деревья, спецификации, иерархии справочников | Код длиннее, легче ошибиться с порядком |
| Повторная выгрузка результата запроса | Дерево построено запросом и результат ещё доступен | Не подходит для дерева, изменённого в коде |
Дерево на форме, итоги групп и другие особые случаи
Дерево — реквизит управляемой формы
На форме реквизит с типом «Дерево значений» живёт как ДанныеФормыДерево. Это другой тип: у него нет Строки, подчинённые строки получают методом ПолучитьЭлементы(). Код из разделов выше на нём не сработает.
Проще всего перейти на сервер и превратить реквизит в обычное дерево:
&НаСервере
Функция ТаблицаИзДереваФормы()
Дерево = РеквизитФормыВЗначение("ДеревоДанных");
Возврат ДеревоВТаблицу(Дерево);
КонецФункции
Если результат должен оказаться в реквизите формы типа «Таблица значений», после выгрузки вызовите ЗначениеВРеквизитФормы(Таблица, "ТаблицаДанных"). Колонки реквизита должны быть созданы заранее в конфигураторе или программно.
Суммы групп задваиваются
Дерево из запроса с ИТОГИ содержит в строках группировок суммы по подчинённым строкам. При выгрузке всех строк итог по колонке Сумма в таблице окажется кратно больше реального: каждая сумма посчитана один раз в детальной строке и ещё раз в каждой группе над ней.
Перед тем как считать итоги по выгруженной таблице, оставьте только детальные строки или обнулите суммы в группах. Отличить группу от записи помогает условие «есть подчинённые строки» или служебная колонка Уровень_, если глубина фиксирована.
Строки удаляют или меняют прямо при обходе
Не удаляйте строки дерева внутри Для Каждого по той же коллекции: часть строк будет пропущена. Сначала выгрузите таблицу, потом правьте дерево обратным циклом по индексу:
Количество = СтрокиДерева.Количество();
Для Сч = 1 По Количество Цикл
СтрокаДерева = СтрокиДерева[Количество - Сч];
Если СтрокаДерева.Сумма = 0 Тогда
СтрокиДерева.Удалить(СтрокаДерева);
КонецЕсли;
КонецЦикла;
Типичные сообщения при выгрузке
Частые вопросы
Можно ли выгрузить только один уровень дерева, например второй?
Как выгрузить содержимое одной группы, а не всё дерево?
Дерево.Строки.Найти(Значение, "Колонка", Истина): третий параметр включает поиск по подчинённым строкам. Затем передайте в процедуру выгрузки НайденнаяСтрока.Строки.Подойдёт ли выгруженная таблица для запроса?
ПОМЕСТИТЬ во временную таблицу. Колонки без типа запрос не примет.