Разработка и Конфигурация · 12 мин
1С: таблица значений как дерево — построение и работа
Как преобразовать таблицу значений 1С в дерево значений: методы построения, обход строк, вывод в форму, типичные ошибки и готовые примеры кода.
Задача отобразить таблицу значений в виде дерева возникает в 1С постоянно: иерархические справочники, группировки отчётов, вложенные строки документов, структура подразделений. Прямого метода «превратить таблицу в дерево» у платформы нет — разработчик строит объект ДеревоЗначений самостоятельно, перенося данные из плоской таблицы в иерархическую структуру по правилу связи «родитель — потомок».
Ключевое различие двух объектов: ТаблицаЗначений — это плоский набор строк с одинаковым набором колонок, а ДеревоЗначений — те же строки, но у каждой есть коллекция Строки с вложенными элементами и ссылка Родитель. Именно наличие вложенных коллекций позволяет выводить данные в элемент формы «Дерево» с разворачиванием уровней.
Ниже разобраны рабочие способы построения дерева из таблицы значений: через рекурсивный обход по ссылке родителя, через запрос с итогами и через ручную группировку. Каждый способ имеет свои условия применимости и ограничения по производительности.
Чем таблица значений отличается от дерева значений
Таблица значений хранит строки линейно: у каждой строки есть индекс и набор значений колонок, но нет понятия вложенности. Поиск по таблице выполняется методами Найти, НайтиСтроки, обход — простым циклом Для Каждого Строка Из ТЗ Цикл.
Дерево значений добавляет к этому иерархию. У каждой строки есть свойство Родитель (строка верхнего уровня или Неопределено для корневых) и коллекция Строки, содержащая подчинённые строки. Обход дерева требует рекурсии или явного стека, потому что глубина вложенности заранее неизвестна.
Практическое следствие: если данные нужно только прочитать и отфильтровать — достаточно таблицы значений. Если данные надо показать пользователю с разворачивающимися уровнями, посчитать итоги по веткам или обойти иерархию сверху вниз — нужно дерево.
Способ 1: построение дерева рекурсивным обходом по родителю
Классический сценарий: в таблице значений есть колонка со ссылкой на элемент и колонка со ссылкой на его родителя (например, выгрузка иерархического справочника). Дерево строится рекурсивной процедурой, которая для каждого узла ищет в таблице все строки с заданным родителем.
Функция ПостроитьДерево(ТЗ)
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Ссылка");
Дерево.Колонки.Добавить("Наименование");
ДобавитьВетку(Дерево.Строки, ТЗ, Справочники.Номенклатура.ПустаяСсылка());
Возврат Дерево;
КонецФункции
Процедура ДобавитьВетку(СтрокиДерева, ТЗ, Родитель)
Отбор = Новый Структура("Родитель", Родитель);
Найденные = ТЗ.НайтиСтроки(Отбор);
Для Каждого СтрокаТЗ Из Найденные Цикл
НоваяСтрока = СтрокиДерева.Добавить();
НоваяСтрока.Ссылка = СтрокаТЗ.Ссылка;
НоваяСтрока.Наименование = СтрокаТЗ.Наименование;
ДобавитьВетку(НоваяСтрока.Строки, ТЗ, СтрокаТЗ.Ссылка);
КонецЦикла;
КонецПроцедуры
У этого способа есть серьёзный недостаток производительности: НайтиСтроки каждый раз сканирует всю таблицу, поэтому на больших данных время работы растёт квадратично. Для таблицы в несколько тысяч строк это ещё допустимо, для десятков тысяч — уже заметно.
Ускорение через соответствие
Оптимизация состоит в предварительной индексации таблицы: строится соответствие, где ключ — ссылка родителя, значение — массив строк-потомков. Тогда поиск потомков выполняется за константное время, а не полным сканированием.
Индекс = Новый Соответствие;
Для Каждого СтрокаТЗ Из ТЗ Цикл
Список = Индекс[СтрокаТЗ.Родитель];
Если Список = Неопределено Тогда
Список = Новый Массив;
Индекс.Вставить(СтрокаТЗ.Родитель, Список);
КонецЕсли;
Список.Добавить(СтрокаТЗ);
КонецЦикла;
Дальше рекурсивная процедура берёт потомков из соответствия вместо НайтиСтроки. На больших таблицах выигрыш по времени — кратный.
Способ 2: дерево из запроса с итогами по иерархии
Если данные ещё не выгружены в таблицу, а находятся в базе, эффективнее получить иерархию сразу запросом. Конструкция ИТОГИ ПО Родитель ИЕРАРХИЯ или ИТОГИ ПО Группа ТОЛЬКО ИЕРАРХИЯ возвращает результат, который обходится выборкой с типом обхода ПоГруппировкамСИерархией.
Запрос = Новый Запрос(
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ИТОГИ ПО
| Ссылка ТОЛЬКО ИЕРАРХИЯ");
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
Метод Выгрузить с параметром ПоГруппировкамСИерархией сразу возвращает готовый объект ДеревоЗначений — ручная рекурсия не нужна вообще. Это самый короткий и надёжный путь, когда источник данных — запрос к базе.
Способ 3: перенос плоской таблицы в дерево через соответствие узлов
Когда порядок строк в таблице не гарантирует, что родитель встретится раньше потомка, удобен двухпроходный алгоритм. Сначала для каждой строки таблицы создаётся строка дерева (пока без привязки) и запоминается в соответствии по ссылке. Затем вторым проходом каждая строка переносится в коллекцию своего родителя.
Узлы = Новый Соответствие;
Для Каждого СтрокаТЗ Из ТЗ Цикл
Узел = Дерево.Строки.Добавить();
Узел.Ссылка = СтрокаТЗ.Ссылка;
Узлы.Вставить(СтрокаТЗ.Ссылка, Узел);
КонецЦикла;
Для Каждого СтрокаТЗ Из ТЗ Цикл
Если ЗначениеЗаполнено(СтрокаТЗ.Родитель) Тогда
РодительскийУзел = Узлы[СтрокаТЗ.Родитель];
Если РодительскийУзел <> Неопределено Тогда
Узел = Узлы[СтрокаТЗ.Ссылка];
ПереместитьСтроку(Дерево, Узел, РодительскийУзел);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Перенос строки внутри дерева выполняется не прямым методом, а через создание новой строки в ветке родителя с копированием значений и последующим удалением исходной — либо через промежуточную структуру. Этот способ устойчив к любому порядку строк во входной таблице.
Если таблица приходит из внешнего источника (файл, обмен), перед построением дерева полезно отсортировать её по полю родителя и проверить, что все значения родителей реально присутствуют среди ссылок. «Потерянные» родители — самая частая причина того, что часть веток в дереве не появляется.
Вывод дерева на форму
Для отображения на управляемой форме создаётся реквизит формы типа ДеревоЗначений и элемент формы «Таблица» с видом «Дерево», связанный с этим реквизитом. Заполнение выполняется программно в серверной процедуре.
&НаСервере
Процедура ЗаполнитьДерево()
Дерево = ПостроитьДерево(ПолучитьТаблицуДанных());
ЗначениеВРеквизитФормы(Дерево, "ДеревоНоменклатуры");
КонецПроцедуры
Обратное преобразование — РеквизитФормыВЗначение("ДеревоНоменклатуры") — понадобится, если дерево редактируется пользователем и результат нужно обработать на сервере. Прямое присваивание дерева реквизиту формы не работает: требуется именно ЗначениеВРеквизитФормы.
- 1Добавьте реквизит формы типа ДеревоЗначений и нужные колонки
- 2Перетащите реквизит в дерево элементов формы — появится таблица с видом Дерево
- 3В серверной процедуре постройте ДеревоЗначений любым способом
- 4Выполните ЗначениеВРеквизитФормы(Дерево, ИмяРеквизита)
- 5Проверьте, что у элемента формы свойство Отображение позволяет разворачивать уровни
Обход дерева и подсчёт итогов по веткам
Обход всех строк дерева независимо от глубины делается рекурсией:
Процедура ОбойтиДерево(Строки, Уровень = 0)
Для Каждого Строка Из Строки Цикл
// обработка строки
ОбойтиДерево(Строка.Строки, Уровень + 1);
КонецЦикла;
КонецПроцедуры
Подсчёт итогов по ветке (например, сумма по группе и всем вложенным) делается методом Итог("Сумма", Истина) — второй параметр включает учёт подчинённых строк. Либо итоги накапливаются вручную при обратном обходе снизу вверх.
Типичные ошибки при работе с деревом
Сравнение способов построения дерева
Самый короткий код и готовое дерево сразу. Подходит, когда данные берутся из базы запросом. Не работает для данных из внешних источников.
Простой и наглядный код для небольших таблиц. На больших объёмах заметно тормозит из-за полного сканирования таблицы на каждом узле.
Быстро на любом объёме и устойчиво к порядку строк. Код длиннее, требует аккуратного переноса строк между ветками.
Как проверить корректность построенного дерева
После построения полезно выполнить три контрольные проверки: количество строк в дереве (с учётом всех уровней) должно совпадать с количеством строк исходной таблицы; у каждой некорневой строки свойство Родитель должно быть заполнено; значения ключевых колонок не должны дублироваться на одном уровне, если это не предусмотрено логикой.
Функция КоличествоСтрокДерева(Строки)
Количество = 0;
Для Каждого Строка Из Строки Цикл
Количество = Количество + 1 + КоличествоСтрокДерева(Строка.Строки);
КонецЦикла;
Возврат Количество;
КонецФункции
Если счётчик не сходится с размером исходной таблицы, ищите строки с родителями, которых нет в данных, или дубликаты ссылок.
Что важно запомнить
Прямого преобразования таблицы в дерево нет: используйте запрос с иерархическими итогами и Выгрузить, либо рекурсию по полю родителя, либо соответствие узлов. Для формы применяйте ЗначениеВРеквизитФормы. Всегда проверяйте данные на циклические ссылки и «потерянных» родителей.
Частые вопросы
Можно ли отсортировать дерево значений по колонке?
Да, у коллекции строк дерева есть метод Сортировать, например: Дерево.Строки.Сортировать("Наименование", Истина) — второй параметр включает рекурсивную сортировку всех вложенных уровней. Без него сортируется только текущий уровень.
Почему ДеревоЗначений недоступно на клиенте в тонком клиенте?
Объект ДеревоЗначений относится к серверным типам и не сериализуется в тонком клиенте. Работать с ним нужно в серверных процедурах, а на форму данные передаются через реквизит формы с помощью ЗначениеВРеквизитФормы.
Как преобразовать дерево обратно в таблицу значений?
Создайте таблицу значений с теми же колонками плюс при необходимости колонками «Родитель» и «Уровень», затем рекурсивно обойдите дерево и добавляйте каждую строку в таблицу, заполняя ссылку на родителя из свойства Родитель текущей строки дерева.
Что делать, если в данных несколько корневых элементов?
Это нормальная ситуация: все строки с пустым или отсутствующим родителем добавляются в коллекцию Строки самого дерева как независимые корневые узлы. Дополнительная обработка не требуется — дерево поддерживает любое число корней.
Как развернуть все уровни дерева на форме программно?
Используйте метод элемента формы: Элементы.ДеревоНоменклатуры.Развернуть(СтрокаДерева.ПолучитьИдентификатор(), Истина) для конкретной ветки, либо в цикле пройдите по строкам верхнего уровня реквизита формы и разверните каждую с рекурсией по подчинённым.
Есть ли ограничение на глубину дерева значений?
Жёсткого ограничения платформы на глубину нет, но глубокая рекурсия при обходе может привести к переполнению стека вызовов. На практике иерархии глубиной в десятки уровней работают нормально; при сотнях уровней стоит заменить рекурсию на обход с явным стеком.