К статье
Практический материал

Разработка и Конфигурация · 12 мин

Как обходить дерево в 1С: методы и готовые примеры кода

Практические способы обхода дерева в 1С: рекурсия по иерархическому справочнику, запросы с ИТОГИ ПО и В ИЕРАРХИИ, обход дерева значений, типичные ошибки.

Деревообразные структуры в 1С встречаются повсюду: иерархические справочники (номенклатура, подразделения, статьи затрат), план видов характеристик, дерево значений на форме, структура подчиненных документов. Задача «обойти дерево» возникает при подсчете итогов по ветвям, поиске элемента, выгрузке структуры или построении отчета. Универсального метода нет — выбор зависит от того, где хранятся данные и что нужно получить на выходе.

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

Какие виды деревьев встречаются в 1С

Прежде чем выбирать метод обхода, определите, с какой структурой работаете. От этого зависит и синтаксис, и производительность решения.

  • Иерархический справочник — данные в базе, связь через реквизит Родитель. Обход идет через выборки или запросы.
  • Дерево значений — объект в памяти, коллекция Строки с вложенными строками. Обход — только программный перебор.
  • План счетов / план видов характеристик — иерархия аналогична справочнику, обход через запросы.
  • Результат запроса с обходом по группировке — выборка с типом обхода «По группировкам с иерархией».

Рекурсивный обход справочника через выборку

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

Процедура ОбойтиДеревоСправочника(Родитель, Уровень = 0)

Запрос = Новый Запрос;

Запрос.Текст =

"ВЫБРАТЬ

| Номенклатура.Ссылка КАК Ссылка,

| Номенклатура.Наименование КАК Наименование

|ИЗ

| Справочник.Номенклатура КАК Номенклатура

|ГДЕ

| Номенклатура.Родитель = &Родитель

|

|УПОРЯДОЧИТЬ ПО

| Наименование";

Запрос.УстановитьПараметр("Родитель", Родитель);

Выборка = Запрос.Выполнить().Выбрать();

Пока Выборка.Следующий() Цикл

// Обработка текущего узла

Сообщить(СтрПовторить(" ", Уровень) + Выборка.Наименование);

// Рекурсивный вызов для дочерних элементов

ОбойтиДеревоСправочника(Выборка.Ссылка, Уровень + 1);

КонецЦикла;

КонецПроцедуры

Точка входа — вызов с пустой ссылкой в качестве родителя: ОбойтиДеревоСправочника(Справочники.Номенклатура.ПустаяСсылка()). Параметр Уровень позволяет отслеживать глубину и формировать отступы или ограничивать обход.

Главный недостаток метода — количество запросов равно количеству узлов. На иерархии из нескольких тысяч элементов это создает заметную нагрузку на СУБД. Для больших структур используйте запросные механизмы, описанные ниже.

Обход дерева запросом: ИТОГИ ПО и В ИЕРАРХИИ

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

Запрос = Новый Запрос;

Запрос.Текст =

"ВЫБРАТЬ

| Номенклатура.Ссылка КАК Ссылка,

| Номенклатура.Наименование КАК Наименование,

| 1 КАК КоличествоЭлементов

|ИЗ

| Справочник.Номенклатура КАК Номенклатура

|ИТОГИ

| СУММА(КоличествоЭлементов)

|ПО

| Ссылка ИЕРАРХИЯ";

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

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

ОбойтиВыборкуРекурсивно(Выборка);

Обход результата также делается рекурсивно, но уже без обращений к базе — все данные получены одним запросом:

Процедура ОбойтиВыборкуРекурсивно(Выборка, Уровень = 0)

Пока Выборка.Следующий() Цикл

Сообщить(СтрПовторить(" ", Уровень) + Выборка.Наименование

+ " (" + Выборка.КоличествоЭлементов + ")");

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

ОбойтиВыборкуРекурсивно(Вложенная, Уровень + 1);

КонецЦикла;

КонецПроцедуры

Отбор элементов по ветке: оператор В ИЕРАРХИИ

Если нужно не обойти все дерево, а получить элементы конкретной ветки (например, всю номенклатуру внутри группы со всеми подгруппами), используйте оператор В ИЕРАРХИИ:

Запрос.Текст = 

"ВЫБРАТЬ

| Номенклатура.Ссылка КАК Ссылка

|ИЗ

| Справочник.Номенклатура КАК Номенклатура

|ГДЕ

| Номенклатура.Ссылка В ИЕРАРХИИ(&Группа)";

Это самый быстрый способ выбрать поддерево: СУБД выполняет один запрос вместо рекурсивного спуска. Оператор работает и с параметром-списком групп.

Сравнение вариантов

Рекурсия с выборкой по родителю

Гибкая логика на каждом уровне, но один запрос на каждый узел — медленно на больших иерархиях.

ИТОГИ ПО ... ИЕРАРХИЯ

Один запрос на всё дерево с готовыми итогами по ветвям. Ограничение: сложно модифицировать данные в процессе обхода.

В ИЕРАРХИИ

Лучший выбор для отбора элементов ветки без обхода — плоская выборка одним запросом.

Соединение таблицы с собой

Фиксированная глубина вложенности, подходит только когда число уровней известно заранее.

Обход дерева значений

Дерево значений (ДеревоЗначений) живет в памяти, поэтому запросы к нему неприменимы. Обход выполняется рекурсивным перебором коллекции Строки:

Процедура ОбойтиДеревоЗначений(Строки, Уровень = 0)

Для Каждого Строка Из Строки Цикл

// Работа с текущей строкой

Сообщить(СтрПовторить(" ", Уровень) + Строка.Наименование);

// Спуск на уровень ниже

ОбойтиДеревоЗначений(Строка.Строки, Уровень + 1);

КонецЦикла;

КонецПроцедуры

// Вызов: ОбойтиДеревоЗначений(Дерево.Строки);

Поиск строки без полного обхода

Если задача — найти строку по значению колонки, полный обход не нужен. У дерева значений есть метод Строки.Найти(Значение, "ИмяКолонки", Истина), где четвертый параметр включает поиск по всем уровням вложенности. Это значительно быстрее ручного перебора.

Для удаления или изменения найденных строк помните: строка знает своего родителя (Строка.Родитель), поэтому можно подняться наверх без повторного обхода. У корневых строк свойство Родитель равно Неопределено.

Порядок действий при выборе метода
  1. 1
    Определите, где лежат данные: база (справочник) или память (дерево значений)
  2. 2
    Оцените объем: до нескольких сотен узлов допустима рекурсия с выборками, больше — только запрос
  3. 3
    Если нужна вся ветка плоским списком — используйте В ИЕРАРХИИ
  4. 4
    Если нужны итоги по каждому уровню — ИТОГИ ПО ... ИЕРАРХИЯ
  5. 5
    Проверьте результат на тестовой ветке с известным числом элементов

Обход снизу вверх: от элемента к корню

Обратная задача — получить цепочку родителей элемента, например полный путь группы номенклатуры. Здесь используется цикл с подъемом по свойству Родитель:

Функция ПолучитьПутьЭлемента(Элемент)

Путь = "";

Текущий = Элемент;

Пока ЗначениеЗаполнено(Текущий) Цикл

Путь = "/" + Текущий.Наименование + Путь;

Текущий = Текущий.Родитель;

КонецЦикла;

Возврат Путь;

КонецФункции

Каждое обращение к Родитель через объект — это чтение из базы. Если путь нужен для тысяч элементов, выгоднее один раз выгрузить соответствие «ссылка → родитель» запросом и строить пути в памяти.

Защита от зацикливания и ограничение глубины

Иерархия в базе может быть повреждена: элемент прямо или через цепочку ссылается на самого себя как на родителя. Рекурсивный обход такой структуры завершится переполнением стека и аварийным разрывом сеанса. Добавляйте защиту:

  • Счетчик глубины — прерывайте обход при превышении разумного предела (например, 50–100 уровней для справочников, где реально 5–10).
  • Список посещенных узлов — храните уже обойденные ссылки в соответствии и пропускайте повторы.
  • Проверка при записи — лучшая защита: в подписке на событие ПриЗаписи проверяйте, что новый родитель не является потомком элемента.
Процедура ОбойтиСЗащитой(Родитель, Посещенные, Уровень = 0)

Если Уровень > 100 Тогда

ВызватьИсключение "Превышена глубина обхода: возможно зацикливание иерархии";

КонецЕсли;

// ... выборка и обход ...

// Перед рекурсивным вызовом:

Если Посещенные[Выборка.Ссылка] <> Неопределено Тогда

Возврат; // узел уже обходили

КонецЕсли;

Посещенные.Вставить(Выборка.Ссылка, Истина);

КонецПроцедуры

Типичные ошибки
01
Рекурсивный запрос на каждый узел в большой иерархии — сотни обращений к СУБД вместо одного запроса с ИЕРАРХИЯ
02
Отсутствие защиты от зацикливания — поврежденная иерархия приводит к переполнению стека и падению сеанса
03
Перебор дерева значений ради поиска строки — вместо встроенного метода Найти с поиском по всем уровням
04
Обращение к Родитель в цикле по объектам — лишние чтения из базы вместо одного запроса в соответствие
05
Изменение коллекции Строки во время ее перебора — пропуск или повторная обработка строк

Проверка результата обхода

После реализации убедитесь, что обход покрывает всю структуру и не дублирует узлы. Простой прием — сравнить количество обойденных элементов с контрольным запросом.

Контрольные точки

Как проверить результат

Число обойденных элементов
Норма

Совпадает с количеством по запросу В ИЕРАРХИИ для той же ветки

Тревожный признак

Меньше — часть веток потеряна; больше — узлы обходятся повторно

Порядок уровней
Норма

Дочерние элементы идут сразу после родителя, отступы соответствуют глубине

Тревожный признак

Элементы перемешаны или уровень сбит — ошибка в передаче параметра Уровень

Время выполнения
Норма

Обход всей иерархии занимает секунды

Тревожный признак

Минуты на среднем справочнике — вероятно, запросы выполняются в цикле

Помеченные на удаление
Норма

Поведение задано явно: включены или исключены отбором

Тревожный признак

Помеченные попадают в обход непреднамеренно

Итог

Что важно запомнить

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

Частые вопросы

Можно ли обойти дерево без рекурсии?

Да, через явный стек: помещайте узлы в массив и извлекайте в цикле, добавляя дочерние элементы. Это удобно, когда глубина иерархии велика и есть риск переполнения стека вызовов. По скорости вариант со стеком сопоставим с рекурсией, но устойчивее к глубоким структурам.

Как получить всех родителей элемента одним запросом?

Одним стандартным запросом полную цепочку родителей произвольной глубины получить нельзя — язык запросов 1С не поддерживает рекурсивные CTE. Практичные варианты: цикл по свойству Родитель, соединение таблицы с собой на фиксированное число уровней или хранение дополнительного реквизита с полным путем, который обновляется при записи.

Почему выборка с ИТОГИ ПО ... ИЕРАРХИЯ не показывает вложенные строки?

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

Как обойти дерево значений на управляемой форме?

Через реквизит формы типа ДеревоЗначений: обращайтесь к коллекции через метод ПолучитьЭлементы() у корня и у каждой строки. Рекурсивная процедура аналогична обходу обычного дерева значений, только вместо Строки используется ПолучитьЭлементы().

Что делать, если в иерархии есть помеченные на удаление элементы?

Добавьте в запрос отбор НЕ ПометкаУдаления, если такие элементы не должны участвовать в обходе. Учтите нюанс: если помечена группа, а её дочерние элементы — нет, они останутся в выборке, но их ветка может оборваться. При необходимости исключайте и потомков помеченных групп через условие В ИЕРАРХИИ с отрицанием.

Как выгрузить дерево справочника в дерево значений?

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