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