Как правильно сформировать дерево значений из иерархического справочника с помощью запроса в 1С?

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

При разработке прикладных решений в 1С часто возникает задача получить структуру иерархического справочника (состоящую из групп и элементов) и отобразить её в виде объекта ДеревоЗначений на форме или использовать в дальнейших серверных алгоритмах. Однако при попытке выгрузить результат запроса с конструкцией ИТОГИ ПО Ссылка ИЕРАРХИЯ напрямую в дерево разработчики сталкиваются с появлением дублирующихся строк-листьев или нарушением вложенности элементов.

Разберем подробно причины такого поведения платформы и рассмотрим все практические способы корректного построения дерева значений.

Почему стандартный запрос с предложением ИТОГИ дублирует строки

Проанализируем типовую ошибку. Предположим, написан следующий запрос с расчетом итогов по иерархии:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Тест.Ссылка КАК Ссылка,
    |	Тест.Код КАК Код,
    |	Тест.Наименование КАК Наименование
    |ИЗ
    |	Справочник.Номенклатура КАК Тест
    |ИТОГИ ПО
    |	Ссылка ИЕРАРХИЯ";

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

При выгрузке такого результата в дерево значений под каждым конечным элементом создается еще одна подчиненная строка с тем же значением. Выясним причину этого поведения:

  1. Назначение механизма итогов: предложение ИТОГИ предназначено в первую очередь для расчета агрегатных функций (сумм, количеств), а не для визуализации структуры данных.
  2. Типы формируемых записей: при выполнении ИТОГИ ПО Ссылка ИЕРАРХИЯ формируются три типа строк:
    • Итог по иерархии (вышестоящие родительские папки);
    • Итог по группировке (строка самой группировки для конкретного элемента);
    • Детальная запись (непосредственно строка выборки из таблицы базы данных).
  3. Особенность метода Выгрузить: при использовании параметра ОбходРезультатаЗапроса.ПоГруппировкамСИерархией в коллекцию ДеревоЗначений выгружаются как группировочные записи, так и вложенные в них детальные записи. В итоге конечный элемент справочника дублируется в виде дочернего листа.
  4. Влияние условия ГДЕ ЭтоГруппа = ЛОЖЬ: если отсечь группы на уровне фильтра ГДЕ, СУБД не выберет строки папок. Платформа 1С искусственно достроит родительские узлы в итогах, но все поля в них (кроме группировочного) будут содержать значение NULL, что нарушит сортировку и заполнение реквизитов.

Способ 1. Использование конструкции УПОРЯДОЧИТЬ ПО ... ИЕРАРХИЯ

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Спр.Ссылка КАК Ссылка,
    |	Спр.Код КАК Код,
    |	Спр.Наименование КАК Наименование
    |ИЗ
    |	Справочник.Номенклатура КАК Спр
    |УПОРЯДОЧИТЬ ПО
    |	Спр.Ссылка ИЕРАРХИЯ
    |АВТОУПОРЯДОЧИВАНИЕ";

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

Обратим внимание на ключевые особенности этого подхода:

  1. Мы не используем секцию ИТОГИ.
  2. Условие УПОРЯДОЧИТЬ ПО Спр.Ссылка ИЕРАРХИЯ (или по другому реквизиту с ключевым словом ИЕРАРХИЯ, например УПОРЯДОЧИТЬ ПО Спр.Код ИЕРАРХИЯ) указывает платформе выстроить результат в иерархическую структуру на уровне выборки.
  3. Метод Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией) формирует дерево без создания фиктивных детальных записей-дублей.

Способ 2. Линейный алгоритм построения дерева за один проход через Соответствие

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

Посмотрим на пример реализации:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Спр.Ссылка КАК Ссылка,
    |	Спр.Родитель КАК Родитель,
    |	Спр.Код КАК Код,
    |	Спр.Наименование КАК Наименование,
    |	Спр.ЭтоГруппа КАК ЭтоГруппа
    |ИЗ
    |	Справочник.Номенклатура КАК Спр
    |УПОРЯДОЧИТЬ ПО
    |	Спр.ЭтоГруппа УБЫВ,
    |	Спр.Наименование ВОЗР";

ТЗ = Запрос.Выполнить().Выгрузить();

// Инициализируем результирующее дерево значений
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
Дерево.Колонки.Добавить("Код", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));

// Соответствие для быстрого поиска родительской строки дерева
СоответствиеСтрок = Новый Соответствие;

Для Каждого Стр Из ТЗ Цикл
    Родитель = Стр.Родитель;
    СтрокаРодителя = Неопределено;
    
    Если ЗначениеЗаполнено(Родитель) Тогда
        СтрокаРодителя = СоответствиеСтрок[Родитель];
    КонецЕсли;
    
    Если СтрокаРодителя = Неопределено Тогда
        // Элемент находится в корне дерева
        НоваяСтрока = Дерево.Строки.Добавить();
    Иначе
        // Элемент подчинен найденному родителю
        НоваяСтрока = СтрокаРодителя.Строки.Добавить();
    КонецЕсли;
    
    ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр);
    
    // Регистрируем строку, если это группа (чтобы к ней могли крепиться дочерние узлы)
    Если Стр.ЭтоГруппа Тогда
        СоответствиеСтрок[Стр.Ссылка] = НоваяСтрока;
    КонецЕсли;
КонецЦикла;

Способ 3. Ручной обход выборки с фильтрацией по ТипЗаписиЗапроса

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

Разберем процедуру обхода:


Процедура ЗаполнитьДеревоПоВыборке(Выборка, КоллекцияСтрок)
    
    Пока Выборка.Следующий() Цикл
        // Пропускаем детальные записи, чтобы исключить дублирование листьев
        Если Выборка.ТипЗаписи() = ТипЗаписиЗапроса.ДетальнаяЗапись Тогда
            Продолжить;
        КонецЕсли;
        
        НоваяСтрока = КоллекцияСтрок.Добавить();
        НоваяСтрока.Ссылка = Выборка.Ссылка;
        
        // Рекурсивно обрабатываем подчиненные узлы
        ВыборкаПодчиненные = Выборка.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
        Если ВыборкаПодчиненные.Количество() > 0 Тогда
            ЗаполнитьДеревоПоВыборке(ВыборкаПодчиненные, НоваяСтрока.Строки);
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Способ 4. Программное построение дерева через СКД

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


// Создаем схему компоновки данных
СхемаКД = Новый СхемаКомпоновкиДанных;

ИсточникДанных = СхемаКД.ИсточникиДанных.Добавить();
ИсточникДанных.Имя = "ИсточникДанных1";
ИсточникДанных.ТипИсточникаДанных = "Local";

НаборДанных = СхемаКД.НаборыДанных.Добавить(Тип("НаборДанныхЗапросСхемыКомпоновкиДанных"));
НаборДанных.Имя = "НаборДанных1";
НаборДанных.ИсточникДанных = ИсточникДанных.Имя;
НаборДанных.Запрос = 
    "ВЫБРАТЬ
    |	Спр.Ссылка КАК Ссылка,
    |	Спр.Код КАК Код,
    |	Спр.Наименование КАК Наименование
    |ИЗ
    |	Справочник.Номенклатура КАК Спр";

// Настраиваем доступные поля
ПолеСсылка = НаборДанных.Поля.Добавить(Тип("ПолеНабораДанныхСхемыКомпоновкиДанных"));
ПолеСсылка.Поле = "Ссылка";
ПолеСсылка.ПутьКДанным = "Ссылка";

// Настраиваем структуру: группировка с иерархией
Настройки = СхемаКД.НастройкиПоУмолчанию;
Группировка = Настройки.Структура.Добавить(Тип("ГруппировкаКомпоновкиДанных"));
Группировка.Использование = Истина;
Группировка.ТипГруппировки = ТипГруппировкиКомпоновкиДанных.Иерархия;

ПолеГруппировки = Группировка.ПоляГруппировки.Элементы.Добавить(Тип("ПолеГруппировкиКомпоновкиДанных"));
ПолеГруппировки.Использование = Истина;
ПолеГруппировки.Поле = Новый ПолеКомпоновкиДанных("Ссылка");
ПолеГруппировки.ТипГруппировки = ТипГруппировкиКомпоновкиДанных.Иерархия;

// Выполняем компоновку в дерево значений
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКД, Настройки,,, Тип("ГенераторМакетаКомпоновкиДанных"));

ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки);

ДеревоРезультат = Новый ДеревоЗначений;
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ПроцессорВывода.УстановитьОбъект(ДеревоРезультат);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);

Сравнение и рекомендации

  1. Самый лаконичный способ: если требуется быстро получить дерево без дополнительного кода — используем запрос с УПОРЯДОЧИТЬ ПО ... ИЕРАРХИЯ и выгрузку через ОбходРезультатаЗапроса.ПоГруппировкамСИерархией.
  2. Самый производительный способ: при работе с большими объемами данных на сервере применяем плоскую выборку и однопроходное заполнение через Соответствие.
  3. Для отчетов и пользовательских выборок: используем механизм СКД, который корректно управляет иерархией на уровне группировок.
← На главную