К статье
Практический материал

Разработка и Конфигурация · 12 мин

1С: таблица значений как дерево — построение и работа

Как преобразовать таблицу значений 1С в дерево значений: методы построения, обход строк, вывод в форму, типичные ошибки и готовые примеры кода.

Задача отобразить таблицу значений в виде дерева возникает в 1С постоянно: иерархические справочники, группировки отчётов, вложенные строки документов, структура подразделений. Прямого метода «превратить таблицу в дерево» у платформы нет — разработчик строит объект ДеревоЗначений самостоятельно, перенося данные из плоской таблицы в иерархическую структуру по правилу связи «родитель — потомок».

Ключевое различие двух объектов: ТаблицаЗначений — это плоский набор строк с одинаковым набором колонок, а ДеревоЗначений — те же строки, но у каждой есть коллекция Строки с вложенными элементами и ссылка Родитель. Именно наличие вложенных коллекций позволяет выводить данные в элемент формы «Дерево» с разворачиванием уровней.

Ниже разобраны рабочие способы построения дерева из таблицы значений: через рекурсивный обход по ссылке родителя, через запрос с итогами и через ручную группировку. Каждый способ имеет свои условия применимости и ограничения по производительности.

Чем таблица значений отличается от дерева значений

Таблица значений хранит строки линейно: у каждой строки есть индекс и набор значений колонок, но нет понятия вложенности. Поиск по таблице выполняется методами Найти, НайтиСтроки, обход — простым циклом Для Каждого Строка Из ТЗ Цикл.

Дерево значений добавляет к этому иерархию. У каждой строки есть свойство Родитель (строка верхнего уровня или Неопределено для корневых) и коллекция Строки, содержащая подчинённые строки. Обход дерева требует рекурсии или явного стека, потому что глубина вложенности заранее неизвестна.

Практическое следствие: если данные нужно только прочитать и отфильтровать — достаточно таблицы значений. Если данные надо показать пользователю с разворачивающимися уровнями, посчитать итоги по веткам или обойти иерархию сверху вниз — нужно дерево.

Способ 1: построение дерева рекурсивным обходом по родителю

Классический сценарий: в таблице значений есть колонка со ссылкой на элемент и колонка со ссылкой на его родителя (например, выгрузка иерархического справочника). Дерево строится рекурсивной процедурой, которая для каждого узла ищет в таблице все строки с заданным родителем.

Функция ПостроитьДерево(ТЗ)

Дерево = Новый ДеревоЗначений;

Дерево.Колонки.Добавить("Ссылка");

Дерево.Колонки.Добавить("Наименование");

ДобавитьВетку(Дерево.Строки, ТЗ, Справочники.Номенклатура.ПустаяСсылка());

Возврат Дерево;

КонецФункции

Процедура ДобавитьВетку(СтрокиДерева, ТЗ, Родитель)

Отбор = Новый Структура("Родитель", Родитель);

Найденные = ТЗ.НайтиСтроки(Отбор);

Для Каждого СтрокаТЗ Из Найденные Цикл

НоваяСтрока = СтрокиДерева.Добавить();

НоваяСтрока.Ссылка = СтрокаТЗ.Ссылка;

НоваяСтрока.Наименование = СтрокаТЗ.Наименование;

ДобавитьВетку(НоваяСтрока.Строки, ТЗ, СтрокаТЗ.Ссылка);

КонецЦикла;

КонецПроцедуры

У этого способа есть серьёзный недостаток производительности: НайтиСтроки каждый раз сканирует всю таблицу, поэтому на больших данных время работы растёт квадратично. Для таблицы в несколько тысяч строк это ещё допустимо, для десятков тысяч — уже заметно.

Ускорение через соответствие

Оптимизация состоит в предварительной индексации таблицы: строится соответствие, где ключ — ссылка родителя, значение — массив строк-потомков. Тогда поиск потомков выполняется за константное время, а не полным сканированием.

Индекс = Новый Соответствие;

Для Каждого СтрокаТЗ Из ТЗ Цикл

Список = Индекс[СтрокаТЗ.Родитель];

Если Список = Неопределено Тогда

Список = Новый Массив;

Индекс.Вставить(СтрокаТЗ.Родитель, Список);

КонецЕсли;

Список.Добавить(СтрокаТЗ);

КонецЦикла;

Дальше рекурсивная процедура берёт потомков из соответствия вместо НайтиСтроки. На больших таблицах выигрыш по времени — кратный.

Способ 2: дерево из запроса с итогами по иерархии

Если данные ещё не выгружены в таблицу, а находятся в базе, эффективнее получить иерархию сразу запросом. Конструкция ИТОГИ ПО Родитель ИЕРАРХИЯ или ИТОГИ ПО Группа ТОЛЬКО ИЕРАРХИЯ возвращает результат, который обходится выборкой с типом обхода ПоГруппировкамСИерархией.

Запрос = Новый Запрос(

"ВЫБРАТЬ

| Номенклатура.Ссылка КАК Ссылка,

| Номенклатура.Наименование КАК Наименование

|ИЗ

| Справочник.Номенклатура КАК Номенклатура

|ИТОГИ ПО

| Ссылка ТОЛЬКО ИЕРАРХИЯ");

Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);

Метод Выгрузить с параметром ПоГруппировкамСИерархией сразу возвращает готовый объект ДеревоЗначений — ручная рекурсия не нужна вообще. Это самый короткий и надёжный путь, когда источник данных — запрос к базе.

Способ 3: перенос плоской таблицы в дерево через соответствие узлов

Когда порядок строк в таблице не гарантирует, что родитель встретится раньше потомка, удобен двухпроходный алгоритм. Сначала для каждой строки таблицы создаётся строка дерева (пока без привязки) и запоминается в соответствии по ссылке. Затем вторым проходом каждая строка переносится в коллекцию своего родителя.

Узлы = Новый Соответствие;

Для Каждого СтрокаТЗ Из ТЗ Цикл

Узел = Дерево.Строки.Добавить();

Узел.Ссылка = СтрокаТЗ.Ссылка;

Узлы.Вставить(СтрокаТЗ.Ссылка, Узел);

КонецЦикла;

Для Каждого СтрокаТЗ Из ТЗ Цикл

Если ЗначениеЗаполнено(СтрокаТЗ.Родитель) Тогда

РодительскийУзел = Узлы[СтрокаТЗ.Родитель];

Если РодительскийУзел <> Неопределено Тогда

Узел = Узлы[СтрокаТЗ.Ссылка];

ПереместитьСтроку(Дерево, Узел, РодительскийУзел);

КонецЕсли;

КонецЕсли;

КонецЦикла;

Перенос строки внутри дерева выполняется не прямым методом, а через создание новой строки в ветке родителя с копированием значений и последующим удалением исходной — либо через промежуточную структуру. Этот способ устойчив к любому порядку строк во входной таблице.

Если таблица приходит из внешнего источника (файл, обмен), перед построением дерева полезно отсортировать её по полю родителя и проверить, что все значения родителей реально присутствуют среди ссылок. «Потерянные» родители — самая частая причина того, что часть веток в дереве не появляется.
ПЗ
Практическое замечание

Вывод дерева на форму

Для отображения на управляемой форме создаётся реквизит формы типа ДеревоЗначений и элемент формы «Таблица» с видом «Дерево», связанный с этим реквизитом. Заполнение выполняется программно в серверной процедуре.

&НаСервере

Процедура ЗаполнитьДерево()

Дерево = ПостроитьДерево(ПолучитьТаблицуДанных());

ЗначениеВРеквизитФормы(Дерево, "ДеревоНоменклатуры");

КонецПроцедуры

Обратное преобразование — РеквизитФормыВЗначение("ДеревоНоменклатуры") — понадобится, если дерево редактируется пользователем и результат нужно обработать на сервере. Прямое присваивание дерева реквизиту формы не работает: требуется именно ЗначениеВРеквизитФормы.

Порядок вывода дерева на форму
  1. 1
    Добавьте реквизит формы типа ДеревоЗначений и нужные колонки
  2. 2
    Перетащите реквизит в дерево элементов формы — появится таблица с видом Дерево
  3. 3
    В серверной процедуре постройте ДеревоЗначений любым способом
  4. 4
    Выполните ЗначениеВРеквизитФормы(Дерево, ИмяРеквизита)
  5. 5
    Проверьте, что у элемента формы свойство Отображение позволяет разворачивать уровни

Обход дерева и подсчёт итогов по веткам

Обход всех строк дерева независимо от глубины делается рекурсией:

Процедура ОбойтиДерево(Строки, Уровень = 0)

Для Каждого Строка Из Строки Цикл

// обработка строки

ОбойтиДерево(Строка.Строки, Уровень + 1);

КонецЦикла;

КонецПроцедуры

Подсчёт итогов по ветке (например, сумма по группе и всем вложенным) делается методом Итог("Сумма", Истина) — второй параметр включает учёт подчинённых строк. Либо итоги накапливаются вручную при обратном обходе снизу вверх.

Типичные ошибки при работе с деревом

Типичные ошибки
01
Присваивание ДеревоЗначений реквизиту формы напрямую вместо ЗначениеВРеквизитФормы — данные на форме не появляются
02
Использование НайтиСтроки внутри рекурсии на большой таблице — квадратичное время построения
03
Отсутствие проверки циклических ссылок в данных — переполнение стека и аварийное завершение
04
Построение дерева на клиенте в тонком клиенте — ДеревоЗначений недоступно на клиенте, построение должно выполняться на сервере
05
Потеря строк, у которых родитель отсутствует во входных данных, — ветка молча не попадает в дерево

Сравнение способов построения дерева

Запрос с ИТОГИ ПО ... ИЕРАРХИЯ + Выгрузить

Самый короткий код и готовое дерево сразу. Подходит, когда данные берутся из базы запросом. Не работает для данных из внешних источников.

Рекурсия с НайтиСтроки

Простой и наглядный код для небольших таблиц. На больших объёмах заметно тормозит из-за полного сканирования таблицы на каждом узле.

Соответствие узлов с двумя проходами

Быстро на любом объёме и устойчиво к порядку строк. Код длиннее, требует аккуратного переноса строк между ветками.

Как проверить корректность построенного дерева

После построения полезно выполнить три контрольные проверки: количество строк в дереве (с учётом всех уровней) должно совпадать с количеством строк исходной таблицы; у каждой некорневой строки свойство Родитель должно быть заполнено; значения ключевых колонок не должны дублироваться на одном уровне, если это не предусмотрено логикой.

Функция КоличествоСтрокДерева(Строки)

Количество = 0;

Для Каждого Строка Из Строки Цикл

Количество = Количество + 1 + КоличествоСтрокДерева(Строка.Строки);

КонецЦикла;

Возврат Количество;

КонецФункции

Если счётчик не сходится с размером исходной таблицы, ищите строки с родителями, которых нет в данных, или дубликаты ссылок.

Итог

Что важно запомнить

Прямого преобразования таблицы в дерево нет: используйте запрос с иерархическими итогами и Выгрузить, либо рекурсию по полю родителя, либо соответствие узлов. Для формы применяйте ЗначениеВРеквизитФормы. Всегда проверяйте данные на циклические ссылки и «потерянных» родителей.

Частые вопросы

Можно ли отсортировать дерево значений по колонке?

Да, у коллекции строк дерева есть метод Сортировать, например: Дерево.Строки.Сортировать("Наименование", Истина) — второй параметр включает рекурсивную сортировку всех вложенных уровней. Без него сортируется только текущий уровень.

Почему ДеревоЗначений недоступно на клиенте в тонком клиенте?

Объект ДеревоЗначений относится к серверным типам и не сериализуется в тонком клиенте. Работать с ним нужно в серверных процедурах, а на форму данные передаются через реквизит формы с помощью ЗначениеВРеквизитФормы.

Как преобразовать дерево обратно в таблицу значений?

Создайте таблицу значений с теми же колонками плюс при необходимости колонками «Родитель» и «Уровень», затем рекурсивно обойдите дерево и добавляйте каждую строку в таблицу, заполняя ссылку на родителя из свойства Родитель текущей строки дерева.

Что делать, если в данных несколько корневых элементов?

Это нормальная ситуация: все строки с пустым или отсутствующим родителем добавляются в коллекцию Строки самого дерева как независимые корневые узлы. Дополнительная обработка не требуется — дерево поддерживает любое число корней.

Как развернуть все уровни дерева на форме программно?

Используйте метод элемента формы: Элементы.ДеревоНоменклатуры.Развернуть(СтрокаДерева.ПолучитьИдентификатор(), Истина) для конкретной ветки, либо в цикле пройдите по строкам верхнего уровня реквизита формы и разверните каждую с рекурсией по подчинённым.

Есть ли ограничение на глубину дерева значений?

Жёсткого ограничения платформы на глубину нет, но глубокая рекурсия при обходе может привести к переполнению стека вызовов. На практике иерархии глубиной в десятки уровней работают нормально; при сотнях уровней стоит заменить рекурсию на обход с явным стеком.