Как получить самого верхнего родителя элемента справочника в 1С

· 9 мин

Содержание

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

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

Все примеры — на встроенном языке 1С:Предприятия 8.3, справочник в них называется Номенклатура, замените на свой.

Как устроена иерархия справочника и что считать корнем

Иерархия включается в конфигураторе: у справочника есть свойство Иерархический и Вид иерархии — «Иерархия групп и элементов» либо «Иерархия элементов». От вида зависит, кто может быть родителем.

  • 📁 Иерархия групп и элементов — родителем всегда выступает группа (ЭтоГруппа = Истина). Обычные элементы лежат внутри групп, но сами родителями не бывают.
  • 🗂 Иерархия элементов — родителем может быть любой элемент, групп как отдельной сущности нет.

Связь «ребёнок → родитель» хранится в стандартном поле Родитель. У элемента верхнего уровня оно равно пустой ссылке: Справочники.Номенклатура.ПустаяСсылка().

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

Проверять «пусто ли поле» удобнее через глобальную функцию ЗначениеЗаполнено(): для пустой ссылки она вернёт Ложь. Сравнение с ПустаяСсылка() тоже работает, но ЗначениеЗаполнено короче и читается лучше.

Самый простой способ — цикл по полю «Родитель»

Для одного элемента это базовый приём. Берём текущий элемент и, пока у него есть родитель, поднимаемся на уровень выше:

Функция ПолучитьКорневогоРодителя(Элемент) Экспорт

    Текущий = Элемент;
    Пока ЗначениеЗаполнено(Текущий.Родитель) Цикл
        Текущий = Текущий.Родитель;
    КонецЦикла;

    Возврат Текущий;

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

Как это работает по шагам:

  1. В переменную Текущий кладём исходный элемент.
  2. Пока Текущий.Родитель заполнен — переходим к нему.
  3. Как только родитель пуст, цикл завершается, и в Текущий лежит корень.

Функция корректно отрабатывает и для элемента, который уже находится на верхнем уровне: цикл просто не выполнится ни разу, и вернётся сам элемент. Для пустой ссылки на входе вернётся пустая ссылка — это ожидаемо.

Каждое обращение к Текущий.Родитель — это чтение объекта из базы. На одном элементе с глубиной 3–4 уровня незаметно, но в цикле по тысячам строк такие чтения складываются. Для массовой обработки смотрите раздел про кэш ниже.

Рекурсивная функция получения корня

Тот же алгоритм можно записать рекурсией — функция вызывает саму себя, пока не дойдёт до верха:

Функция КореньРекурсивно(Элемент) Экспорт

    Если Не ЗначениеЗаполнено(Элемент.Родитель) Тогда
        Возврат Элемент;
    КонецЕсли;

    Возврат КореньРекурсивно(Элемент.Родитель);

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

Логика читается почти как определение: «если родителя нет — это и есть корень; иначе корень тот же, что у родителя».

Рекурсия выглядит компактнее цикла, но у неё есть цена:

  • каждый вызов занимает место в стеке вызовов;
  • при аномально большой глубине (сотни уровней) можно упереться в ограничение стека и получить ошибку;
  • отлаживать вложенные вызовы сложнее, чем обычный цикл.

На практике глубина иерархии справочника — это 2–5 уровней, и рекурсия отработает без проблем. Но если выбираете между двумя вариантами для продакшена, цикл надёжнее: он не растит стек и ведёт себя предсказуемо на любой глубине.

Рекурсия оправдана, когда по пути к корню нужно ещё что-то собрать — например, построить всю цепочку предков от элемента до вершины. Для «просто дай корень» цикл проще.

Можно ли достать корень через язык запросов

Короткий ответ: напрямую «подняться до корня» одним запросом для произвольной глубины нельзя. В языке запросов 1С нет рекурсивных CTE, поэтому пройти цепочку Родитель неизвестной длины внутри запроса не получится.

Что запрос делает хорошо — работает в направлении «сверху вниз»:

  • Выбрать все корневые элементы справочника (те, у кого родитель пуст):
ВЫБРАТЬ
    Номенклатура.Ссылка КАК Ссылка
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    Номенклатура.Родитель = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
  • Выбрать всё, что лежит под известным корнем, через условие В ИЕРАРХИИ:
ВЫБРАТЬ
    Номенклатура.Ссылка КАК Ссылка
ИЗ
    Справочник.Номенклатура КАК Номенклатура
ГДЕ
    Номенклатура.Ссылка В ИЕРАРХИИ(&Корень)

Если глубина дерева фиксирована и невелика (скажем, ровно два уровня), можно соединить таблицу справочника саму с собой через Родитель нужное число раз и так добраться до вершины. Но это жёстко завязано на глубину: добавится уровень — запрос перестанет находить корень.

Вывод: для «дай корень конкретного элемента» запрос — не инструмент. Его сила в другом — быстро выгрузить пары «ссылка → родитель» для всех элементов сразу, а подъём до корня сделать уже в коде. Этот приём разберём дальше.

Массовая обработка: кэшируем родителей в «Соответствии»

Когда корень нужен не для одного элемента, а для тысяч строк (пересчёт, отчёт, групповая обработка), обращаться к .Родитель в цикле — дорого. Каждое обращение бьёт в базу.

Решение — один запрос забирает связи «ссылка → родитель» для всего справочника, кладём их в Соответствие, и дальше поднимаемся до корня целиком в памяти:

Функция ПостроитьКэшРодителей() Экспорт

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

    Кэш = Новый Соответствие;
    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        Кэш.Вставить(Выборка.Ссылка, Выборка.Родитель);
    КонецЦикла;

    Возврат Кэш;

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

Теперь корень любого элемента ищется без единого обращения к базе — только по карте в памяти:

Функция КореньПоКэшу(Элемент, Кэш) Экспорт

    Текущий = Элемент;
    Родитель = Кэш.Получить(Текущий);
    Пока ЗначениеЗаполнено(Родитель) Цикл
        Текущий = Родитель;
        Родитель = Кэш.Получить(Текущий);
    КонецЦикла;

    Возврат Текущий;

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

Схема применения: один раз вызвали ПостроитьКэшРодителей(), а потом гоняете КореньПоКэшу() по всем нужным элементам. На большом объёме разница в скорости — на порядки, потому что из базы читаем ровно один раз.

СпособКогда применятьОбращений к базе
Цикл по .Родительодин-несколько элементовпо одному на уровень
Рекурсиянужна вся цепочка предковпо одному на уровень
Запроснайти корни или потомков корняодин
Кэш в Соответствиимассовая обработка многих элементоводин на весь справочник

Производительность и обработка нештатных ситуаций

Даже рабочий алгоритм стоит защитить от крайних случаев. Что учесть:

  • 🔁 Зацикленная ссылка. В штатном справочнике элемент не может быть предком самого себя, но после некорректной загрузки данных или ручного вмешательства такое встречается. Бесконечный цикл подвесит фоновое задание.
  • 🈳 Пустая ссылка на входе. Если в функцию прилетела пустая ссылка, вернётся она же. Решите заранее, допустимо ли это для вызывающего кода.
  • 📏 Глубина. Для рекурсии аномальная глубина грозит переполнением стека; для цикла — просто долгой работой.

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

Функция КореньБезопасно(Элемент) Экспорт

    Текущий = Элемент;
    Счётчик = 0;
    Пока ЗначениеЗаполнено(Текущий.Родитель) Цикл
        Текущий = Текущий.Родитель;
        Счётчик = Счётчик + 1;
        Если Счётчик > 1000 Тогда
            ВызватьИсключение "Похоже на зацикленную иерархию: " + Элемент;
        КонецЕсли;
    КонецЦикла;

    Возврат Текущий;

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

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

Если корень нужен часто в рамках одного вызова, посчитайте его один раз и положите в переменную, а не дёргайте функцию повторно для тех же элементов.

Переиспользование функции и полная цепочка предков

Если корень нужен в разных местах конфигурации, не копируйте код по модулям форм и объектов. Вынесите функцию в общий модуль.

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

Корень = ОбщегоНазначенияСервер.ПолучитьКорневогоРодителя(Элемент);

Имя модуля подставьте своё. Такой подход держит логику в одном месте: поправили алгоритм один раз — он изменился везде, а не в десяти копиях.

Функция читает поле Родитель, то есть обращается к данным. Не вызывайте её на клиенте напрямую — только на сервере или через модуль с флагом «Вызов сервера», иначе клиент не получит доступ к объекту.

Иногда корня мало — нужна вся дорога от элемента до вершины: для построения хлебных крошек, печати пути или анализа вложенности. Собрать её можно тем же подъёмом, только складывая каждый уровень в массив:

Функция ЦепочкаДоКорня(Элемент) Экспорт

    Путь = Новый Массив;
    Текущий = Элемент;
    Пока ЗначениеЗаполнено(Текущий) Цикл
        Путь.Вставить(0, Текущий);
        Текущий = Текущий.Родитель;
    КонецЦикла;

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

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

Метод Вставить(0, …) кладёт каждый новый уровень в начало массива, поэтому на выходе элементы идут в естественном порядке — от корня к исходному элементу. Первый элемент массива Путь[0] и будет корневым родителем.

Из такого массива легко собрать строку пути:

Представление = "";
Для Каждого Узел Из ЦепочкаДоКорня(Элемент) Цикл
    Представление = Представление + ?(Представление = "", "", " / ") + Узел.Наименование;
КонецЦикла;

Получится строка вида Группа / Подгруппа / Элемент — удобно для отчётов и логов.

Частые ошибки

Что чаще всего ломает получение корневого родителя:

  • ❌ Сравнение не с той пустой ссылкой. Пустую ссылку надо брать именно по своему справочнику: Справочники.Номенклатура.ПустаяСсылка(). Пустая ссылка другого типа условию не удовлетворит, и цикл отработает неверно.
  • ❌ Проверка на Неопределено вместо пустой ссылки. Поле Родитель у элемента верхнего уровня — это не Неопределено, а именно пустая ссылка справочника. Проверка = Неопределено не поймает корень.
  • ❌ Обращение к .Родитель в цикле по тысячам строк. Работает, но медленно: каждое чтение — запрос в базу. На объёме переходите на кэш в Соответствии.
  • ❌ Путаница групп и элементов. В виде «иерархия групп и элементов» корнем для обычного элемента будет группа. Если по логике вам нужна именно верхняя группа — это ровно то, что вернут функции выше; дополнительной проверки ЭтоГруппа обычно не требуется.
Проверьте себя
Чему равно поле `Родитель` у элемента верхнего уровня иерархического справочника?
Какой способ правильно выбрать для подъёма до корня у тысяч элементов сразу?
Есть ли в платформе 1С 8.3 встроенная функция, сразу возвращающая корневого родителя?

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

Есть ли в 1С готовая функция для получения корневого родителя?
Нет. Отдельного встроенного метода платформа не предоставляет. Корень получают самостоятельно — циклом по полю Родитель, рекурсией или через заранее построенный кэш связей.
Чем отличается корневой родитель от просто родителя?
Родитель — это элемент на один уровень выше. Корневой родитель — самый верхний в цепочке, у которого поле Родитель уже пустое. Чтобы его получить, нужно подняться по всем уровням, а не на один.
Что вернётся, если у элемента нет родителя?
Сам элемент. Он уже находится на верхнем уровне и является собственным корнем — цикл или рекурсия просто не делают ни одного шага вверх.
Как быстро обработать корень сразу для многих элементов?
Выгрузите пары «ссылка → родитель» для всего справочника одним запросом в Соответствие, а затем поднимайтесь до корня в памяти. Так база читается один раз, а не на каждом шаге.
Можно ли получить корень одним запросом?
Для произвольной глубины — нет: рекурсивных CTE в языке запросов 1С нет. Запросом удобно выбрать все корневые элементы или всех потомков известного корня через В ИЕРАРХИИ, а подъём к корню для конкретного элемента делают в коде.