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