Как получить самого верхнего родителя элемента справочника в 1С
Содержание
Верхний (корневой) родитель — это элемент иерархии, у которого поле Родитель пустое. Чтобы получить его для любого элемента справочника в 1С 8.3, идут вверх по цепочке Родитель до первого, у кого родителя уже нет. Встроенной функции вроде «найти корень» в платформе нет — корневого родителя достают циклом, рекурсией или запросом.
Ниже — четыре рабочих способа и разбор, какой когда применять. Для единичного элемента хватит короткого цикла. Для обработки тысяч строк цикл с обращением к .Родитель начнёт тормозить — там выручает запрос и кэш в Соответствии.
Все примеры — на встроенном языке 1С:Предприятия 8.3, справочник в них называется Номенклатура, замените на свой.
Как устроена иерархия справочника и что считать корнем
Иерархия включается в конфигураторе: у справочника есть свойство Иерархический и Вид иерархии — «Иерархия групп и элементов» либо «Иерархия элементов». От вида зависит, кто может быть родителем.
- 📁 Иерархия групп и элементов — родителем всегда выступает группа (
ЭтоГруппа = Истина). Обычные элементы лежат внутри групп, но сами родителями не бывают. - 🗂 Иерархия элементов — родителем может быть любой элемент, групп как отдельной сущности нет.
Связь «ребёнок → родитель» хранится в стандартном поле Родитель. У элемента верхнего уровня оно равно пустой ссылке: Справочники.Номенклатура.ПустаяСсылка().
Корневой родитель — это элемент, поднявшись до которого, вы упираетесь в пустое поле Родитель. Если у переданного элемента родитель уже пуст, он сам и есть свой корень — возвращаем его без изменений.
Проверять «пусто ли поле» удобнее через глобальную функцию ЗначениеЗаполнено(): для пустой ссылки она вернёт Ложь. Сравнение с ПустаяСсылка() тоже работает, но ЗначениеЗаполнено короче и читается лучше.
Самый простой способ — цикл по полю «Родитель»
Для одного элемента это базовый приём. Берём текущий элемент и, пока у него есть родитель, поднимаемся на уровень выше:
Функция ПолучитьКорневогоРодителя(Элемент) Экспорт
Текущий = Элемент;
Пока ЗначениеЗаполнено(Текущий.Родитель) Цикл
Текущий = Текущий.Родитель;
КонецЦикла;
Возврат Текущий;
КонецФункции
Как это работает по шагам:
- В переменную
Текущийкладём исходный элемент. - Пока
Текущий.Родительзаполнен — переходим к нему. - Как только родитель пуст, цикл завершается, и в
Текущийлежит корень.
Функция корректно отрабатывает и для элемента, который уже находится на верхнем уровне: цикл просто не выполнится ни разу, и вернётся сам элемент. Для пустой ссылки на входе вернётся пустая ссылка — это ожидаемо.
Каждое обращение к Текущий.Родитель — это чтение объекта из базы. На одном элементе с глубиной 3–4 уровня незаметно, но в цикле по тысячам строк такие чтения складываются. Для массовой обработки смотрите раздел про кэш ниже.
Рекурсивная функция получения корня
Тот же алгоритм можно записать рекурсией — функция вызывает саму себя, пока не дойдёт до верха:
Функция КореньРекурсивно(Элемент) Экспорт
Если Не ЗначениеЗаполнено(Элемент.Родитель) Тогда
Возврат Элемент;
КонецЕсли;
Возврат КореньРекурсивно(Элемент.Родитель);
КонецФункции
Логика читается почти как определение: «если родителя нет — это и есть корень; иначе корень тот же, что у родителя».
Рекурсия выглядит компактнее цикла, но у неё есть цена:
- каждый вызов занимает место в стеке вызовов;
- при аномально большой глубине (сотни уровней) можно упереться в ограничение стека и получить ошибку;
- отлаживать вложенные вызовы сложнее, чем обычный цикл.
На практике глубина иерархии справочника — это 2–5 уровней, и рекурсия отработает без проблем. Но если выбираете между двумя вариантами для продакшена, цикл надёжнее: он не растит стек и ведёт себя предсказуемо на любой глубине.
Рекурсия оправдана, когда по пути к корню нужно ещё что-то собрать — например, построить всю цепочку предков от элемента до вершины. Для «просто дай корень» цикл проще.
Можно ли достать корень через язык запросов
Короткий ответ: напрямую «подняться до корня» одним запросом для произвольной глубины нельзя. В языке запросов 1С нет рекурсивных CTE, поэтому пройти цепочку Родитель неизвестной длины внутри запроса не получится.
Что запрос делает хорошо — работает в направлении «сверху вниз»:
- Выбрать все корневые элементы справочника (те, у кого родитель пуст):
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
Номенклатура.Родитель = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
- Выбрать всё, что лежит под известным корнем, через условие
В ИЕРАРХИИ:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
Номенклатура.Ссылка В ИЕРАРХИИ(&Корень)
Если глубина дерева фиксирована и невелика (скажем, ровно два уровня), можно соединить таблицу справочника саму с собой через Родитель нужное число раз и так добраться до вершины. Но это жёстко завязано на глубину: добавится уровень — запрос перестанет находить корень.
Вывод: для «дай корень конкретного элемента» запрос — не инструмент. Его сила в другом — быстро выгрузить пары «ссылка → родитель» для всех элементов сразу, а подъём до корня сделать уже в коде. Этот приём разберём дальше.
Массовая обработка: кэшируем родителей в «Соответствии»
Когда корень нужен не для одного элемента, а для тысяч строк (пересчёт, отчёт, групповая обработка), обращаться к .Родитель в цикле — дорого. Каждое обращение бьёт в базу.
Решение — один запрос забирает связи «ссылка → родитель» для всего справочника, кладём их в Соответствие, и дальше поднимаемся до корня целиком в памяти:
Функция ПостроитьКэшРодителей() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Родитель КАК Родитель
|ИЗ
| Справочник.Номенклатура КАК Номенклатура";
Кэш = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Кэш.Вставить(Выборка.Ссылка, Выборка.Родитель);
КонецЦикла;
Возврат Кэш;
КонецФункции
Теперь корень любого элемента ищется без единого обращения к базе — только по карте в памяти:
Функция КореньПоКэшу(Элемент, Кэш) Экспорт
Текущий = Элемент;
Родитель = Кэш.Получить(Текущий);
Пока ЗначениеЗаполнено(Родитель) Цикл
Текущий = Родитель;
Родитель = Кэш.Получить(Текущий);
КонецЦикла;
Возврат Текущий;
КонецФункции
Схема применения: один раз вызвали ПостроитьКэшРодителей(), а потом гоняете КореньПоКэшу() по всем нужным элементам. На большом объёме разница в скорости — на порядки, потому что из базы читаем ровно один раз.
| Способ | Когда применять | Обращений к базе |
|---|---|---|
Цикл по .Родитель | один-несколько элементов | по одному на уровень |
| Рекурсия | нужна вся цепочка предков | по одному на уровень |
| Запрос | найти корни или потомков корня | один |
Кэш в Соответствии | массовая обработка многих элементов | один на весь справочник |
Производительность и обработка нештатных ситуаций
Даже рабочий алгоритм стоит защитить от крайних случаев. Что учесть:
- 🔁 Зацикленная ссылка. В штатном справочнике элемент не может быть предком самого себя, но после некорректной загрузки данных или ручного вмешательства такое встречается. Бесконечный цикл подвесит фоновое задание.
- 🈳 Пустая ссылка на входе. Если в функцию прилетела пустая ссылка, вернётся она же. Решите заранее, допустимо ли это для вызывающего кода.
- 📏 Глубина. Для рекурсии аномальная глубина грозит переполнением стека; для цикла — просто долгой работой.
Подстелить соломку помогает счётчик-ограничитель. Если за разумное число шагов корень не найден — значит, в данных цикл, и лучше прервать работу с понятной ошибкой:
Функция КореньБезопасно(Элемент) Экспорт
Текущий = Элемент;
Счётчик = 0;
Пока ЗначениеЗаполнено(Текущий.Родитель) Цикл
Текущий = Текущий.Родитель;
Счётчик = Счётчик + 1;
Если Счётчик > 1000 Тогда
ВызватьИсключение "Похоже на зацикленную иерархию: " + Элемент;
КонецЕсли;
КонецЦикла;
Возврат Текущий;
КонецФункции
Порог 1000 здесь взят с большим запасом — реальная иерархия редко глубже десятка уровней, так что исключение сработает только на действительно битых данных, а не на нормальной вложенности.
Если корень нужен часто в рамках одного вызова, посчитайте его один раз и положите в переменную, а не дёргайте функцию повторно для тех же элементов.
Переиспользование функции и полная цепочка предков
Если корень нужен в разных местах конфигурации, не копируйте код по модулям форм и объектов. Вынесите функцию в общий модуль.
Для серверной обработки данных поставьте у модуля флаги «Сервер» и при необходимости «Вызов сервера», а у самой функции — ключевое слово Экспорт (в примерах выше оно уже проставлено). Тогда вызов выглядит единообразно из любого места:
Корень = ОбщегоНазначенияСервер.ПолучитьКорневогоРодителя(Элемент);
Имя модуля подставьте своё. Такой подход держит логику в одном месте: поправили алгоритм один раз — он изменился везде, а не в десяти копиях.
Функция читает поле Родитель, то есть обращается к данным. Не вызывайте её на клиенте напрямую — только на сервере или через модуль с флагом «Вызов сервера», иначе клиент не получит доступ к объекту.
Иногда корня мало — нужна вся дорога от элемента до вершины: для построения хлебных крошек, печати пути или анализа вложенности. Собрать её можно тем же подъёмом, только складывая каждый уровень в массив:
Функция ЦепочкаДоКорня(Элемент) Экспорт
Путь = Новый Массив;
Текущий = Элемент;
Пока ЗначениеЗаполнено(Текущий) Цикл
Путь.Вставить(0, Текущий);
Текущий = Текущий.Родитель;
КонецЦикла;
Возврат Путь;
КонецФункции
Метод Вставить(0, …) кладёт каждый новый уровень в начало массива, поэтому на выходе элементы идут в естественном порядке — от корня к исходному элементу. Первый элемент массива Путь[0] и будет корневым родителем.
Из такого массива легко собрать строку пути:
Представление = "";
Для Каждого Узел Из ЦепочкаДоКорня(Элемент) Цикл
Представление = Представление + ?(Представление = "", "", " / ") + Узел.Наименование;
КонецЦикла;
Получится строка вида Группа / Подгруппа / Элемент — удобно для отчётов и логов.
Частые ошибки
Что чаще всего ломает получение корневого родителя:
- ❌ Сравнение не с той пустой ссылкой. Пустую ссылку надо брать именно по своему справочнику:
Справочники.Номенклатура.ПустаяСсылка(). Пустая ссылка другого типа условию не удовлетворит, и цикл отработает неверно. - ❌ Проверка на
Неопределеновместо пустой ссылки. ПолеРодительу элемента верхнего уровня — это неНеопределено, а именно пустая ссылка справочника. Проверка= Неопределеноне поймает корень. - ❌ Обращение к
.Родительв цикле по тысячам строк. Работает, но медленно: каждое чтение — запрос в базу. На объёме переходите на кэш вСоответствии. - ❌ Путаница групп и элементов. В виде «иерархия групп и элементов» корнем для обычного элемента будет группа. Если по логике вам нужна именно верхняя группа — это ровно то, что вернут функции выше; дополнительной проверки
ЭтоГруппаобычно не требуется.
Частые вопросы
Есть ли в 1С готовая функция для получения корневого родителя?
Родитель, рекурсией или через заранее построенный кэш связей.Чем отличается корневой родитель от просто родителя?
Родитель — это элемент на один уровень выше. Корневой родитель — самый верхний в цепочке, у которого поле Родитель уже пустое. Чтобы его получить, нужно подняться по всем уровням, а не на один.Что вернётся, если у элемента нет родителя?
Как быстро обработать корень сразу для многих элементов?
Соответствие, а затем поднимайтесь до корня в памяти. Так база читается один раз, а не на каждом шаге.Можно ли получить корень одним запросом?
В ИЕРАРХИИ, а подъём к корню для конкретного элемента делают в коде.