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