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