Поднимаемся к корню иерархии: верхний родитель в 1С

· 9 мин

Содержание

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

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

Где в 1С вообще есть «родитель»

Реквизит Родитель существует только у объектов с включённой иерархией. Это:

  • 📁 Справочники — самый частый случай (Справочник.Номенклатура, Справочник.Контрагенты и т. д.).
  • 📊 Планы счетов — иерархия «счёт → субсчёт».
  • 🔑 Планы видов характеристик — если у них настроена иерархия.

У документов, регистров, перечислений и обработок реквизита Родитель нет — у них нет собственной иерархии. Когда говорят «верхний родитель для документа», имеют в виду родителя значения, которое лежит в реквизите документа или в измерении регистра (чаще всего это ссылка на иерархический справочник). Про этот случай — отдельный раздел ниже.

Иерархия у справочника бывает двух видов:

Вид иерархииЧто лежит в веткахРеквизит ЭтоГруппа
Групп и элементовГруппы и внутри них элементыЕсть, различает группу и элемент
ЭлементовЛюбой элемент может быть родителемОтсутствует

Это важно: обращаться к ЭтоГруппа можно только у справочника с иерархией групп и элементов. У иерархии элементов такого реквизита нет, и попытка его прочитать вызовет ошибку.

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

Реквизит «Родитель» и один шаг вверх

Базовая операция — получить непосредственного родителя. Это просто обращение к реквизиту:

Непосредственный = Элемент.Родитель;

Если Элемент — корневой, вернётся пустая ссылка того же типа, а не Неопределено. Проверять заполненность надо так:

Если ЗначениеЗаполнено(Элемент.Родитель) Тогда
    // есть родитель, можно подниматься выше
Иначе
    // это уже корень ветки
КонецЕсли;

ЗначениеЗаполнено() корректно отличает пустую ссылку от заполненной. Сравнение = Неопределено здесь не сработает — пустая ссылка это не Неопределено.

Если нужно понять, группа перед вами или конечный элемент (в иерархии групп и элементов):

Если Элемент.ЭтоГруппа Тогда
    // это папка
КонецЕсли;

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

Подъём до корня циклом

Самый надёжный и читаемый способ — подниматься по Родитель, пока он не опустеет. Это общая функция, которая работает с любым иерархическим справочником:

Функция ВерхнийРодитель(Элемент) Экспорт
    Текущий = Элемент;
    Пока ЗначениеЗаполнено(Текущий.Родитель) Цикл
        Текущий = Текущий.Родитель;
    КонецЦикла;
    Возврат Текущий;
КонецФункции

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

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

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

Часто вместе с корнем нужен и весь путь от элемента до вершины — например, чтобы вывести «хлебные крошки» группы или проверить, на каком уровне лежит элемент. Путь собирается тем же циклом, по дороге наверх:

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

Первый элемент массива — корень, последний — исходный элемент. Если нужен порядок сверху вниз, вставляйте в начало (Вставить(0, ...)), как в примере; если снизу вверх — просто Добавить().

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

Как получить корень запросом

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

Проблема в том, что язык запросов 1С не умеет рекурсию (нет аналога рекурсивного CTE). Поэтому «подняться на произвольную глубину одним запросом» нельзя. Но есть рабочий приём: если максимальная глубина иерархии известна и невелика, разворачиваем цепочку родителей через точку до нужного уровня.

ВЫБРАТЬ
    Товары.Ссылка КАК Товар,
    Товары.Родитель КАК Уровень1,
    Товары.Родитель.Родитель КАК Уровень2,
    Товары.Родитель.Родитель.Родитель КАК Уровень3
ИЗ
    Справочник.Номенклатура КАК Товары
ГДЕ
    Товары.ЭтоГруппа = ЛОЖЬ

Платформа сама добавит нужные соединения справочника с самим собой. Дальше в коде берём для каждой строки последний заполненный уровень — это и есть корень. Добавляйте столько уровней, сколько реально бывает в вашей базе, плюс один-два про запас.

Для отбора «всё, что лежит внутри конкретной группы», рекурсия не нужна вовсе — работает оператор В ИЕРАРХИИ:

ВЫБРАТЬ
    Товары.Ссылка
ИЗ
    Справочник.Номенклатура КАК Товары
ГДЕ
    Товары.Ссылка В ИЕРАРХИИ(&ВерхняяГруппа)

А если нужны итоги с разворотом по уровням дерева — в секции ИТОГИ есть ключевое слово ИЕРАРХИЯ:

ИТОГИ
    СУММА(Количество)
ПО
    Номенклатура ИЕРАРХИЯ

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

Когда иерархия лежит в реквизите документа или в измерении регистра

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

Логика та же, меняется только точка входа:

// товар из строки табличной части документа
Товар = СтрокаТЧ.Номенклатура;
КореньГруппы = ВерхнийРодитель(Товар);

Для регистра удобнее сразу поднять родителя в запросе к регистру, развернув измерение через точку:

ВЫБРАТЬ
    Движения.Номенклатура КАК Товар,
    Движения.Номенклатура.Родитель КАК Группа,
    Движения.Количество
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки КАК Движения

Так родитель измерения приедет вместе с данными регистра, без отдельного подъёма в коде. Если нужен именно корень, а не ближайшая группа — разворачивайте цепочку через точку на нужную глубину, как в разделе про запросы.

Верхний родитель в управляемых формах

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

&НаКлиенте
Процедура СписокПриАктивизацииСтроки(Элемент)
    ТекущиеДанные = Элементы.Список.ТекущиеДанные;
    Если ТекущиеДанные <> Неопределено Тогда
        Родитель = ТекущиеДанные.Родитель;
    КонецЕсли;
КонецПроцедуры

Дальше корень вычисляют на сервере — на клиенте обращаться к базе за цепочкой родителей нельзя:

&НаСервереБезКонтекста
Функция КореньДляСтроки(Ссылка)
    Возврат ОбщийМодуль.ВерхнийРодитель(Ссылка);
КонецФункции

Несколько рабочих мелочей по формам:

  • 🌳 Чтобы динамический список показывал дерево, в его настройках должен быть включён показ иерархии (свойство списка). Без этого Родитель в данных есть, а визуального дерева нет.
  • 🔎 Если нужно отобрать по верхней группе — ставьте отбор динамического списка с видом сравнения «В иерархии» по реквизиту, а не фильтруйте выборку вручную.
  • 🚫 Не читайте Родитель в цикле прямо в клиентском коде формы: каждый вызов уйдёт на сервер. Соберите нужные ссылки и сходите на сервер один раз.

Производительность: кэш вместо лишних обращений к базе

Если верхний родитель запрашивается часто, а справочник меняется редко, разумно посчитать соответствие «элемент → корень» один раз и держать его в соответствии:

Функция КартаКорней() Экспорт
    Карта = Новый Соответствие;
    Запрос = Новый Запрос(
        "ВЫБРАТЬ Ссылка, Родитель ИЗ Справочник.Номенклатура");
    Выборка = Запрос.Выполнить().Выбрать();
    Родители = Новый Соответствие;
    Пока Выборка.Следующий() Цикл
        Родители.Вставить(Выборка.Ссылка, Выборка.Родитель);
    КонецЦикла;
    Для Каждого Пара Из Родители Цикл
        Текущий = Пара.Ключ;
        Пока ЗначениеЗаполнено(Родители[Текущий]) Цикл
            Текущий = Родители[Текущий];
        КонецЦикла;
        Карта.Вставить(Пара.Ключ, Текущий);
    КонецЦикла;
    Возврат Карта;
КонецФункции

Здесь вся иерархия поднимается одним запросом в память, а корни считаются по соответствию, без повторных обращений к базе. Для разовой массовой обработки этого достаточно. Если результат нужен между вызовами — его кладут в повторно используемый общий модуль (с настройкой повторного использования возвращаемых значений) и сбрасывают при изменении справочника.

Отдельный способ решить задачу без правки конфигурации — внешняя обработка. В неё выносят ту же функцию подъёма по иерархии, если нет возможности менять типовую конфигурацию или нужно прогнать разовую выгрузку. Логика внутри остаётся такой же, меняется только способ доставки кода в базу.

Частые ошибки при подъёме по иерархии

Ошибки тут повторяются из проекта в проект:

  • ❌ Сравнение с Неопределено. Родитель у корня это пустая ссылка, а не Неопределено. Условие Если Родитель = Неопределено никогда не сработает и цикл уйдёт в вечность. Проверяйте через ЗначениеЗаполнено().
  • ❌ Обращение к ЭтоГруппа там, где его нет. У справочника с иерархией элементов реквизита ЭтоГруппа не существует — чтение вызовет ошибку. Проверьте вид иерархии в конфигураторе.
  • ❌ Подъём по одному в массовой обработке. Тысячи обращений .Родитель в цикле по выборке — это тысячи запросов. Для массовых операций поднимайте данные запросом или кэшем.
  • ❌ Ожидание рекурсии от языка запросов. Запрос не поднимется на произвольную глубину сам. Либо разворачивайте цепочку через точку на известную глубину, либо досчитывайте корень в коде.
  • ❌ Попытка взять Родитель у документа или регистра. Его там нет. Поднимайтесь от значения реквизита или измерения, а не от самого документа.
Какой способ получить верхнего родителя выбрать?

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

Чем `Родитель` отличается от `Владелец`?
Родитель — это иерархия внутри одного справочника (группа → элемент). Владелец — связь подчинённого справочника с главным (например, договоры подчинены контрагенту). Это разные механизмы, подниматься по ним нужно по-разному.
Что вернёт `Родитель` у элемента на верхнем уровне?
Пустую ссылку того же типа, что и справочник. Не Неопределено и не Null. Поэтому заполненность проверяют через ЗначениеЗаполнено().
Можно ли получить всех родителей цепочкой, а не только верхнего?
Да. В том же цикле на каждом шаге складывайте Текущий в массив — получите путь от элемента до корня. Разверните массив, если нужен порядок от корня вниз.
Есть ли риск зациклиться, если иерархия «закольцована»?
При штатной работе платформа не даёт сделать элемент родителем самому себе, так что цикл по Родитель всегда упирается в пустую ссылку. Если данные пришли извне загрузкой и целостность под сомнением — добавьте счётчик шагов с разумным пределом как страховку.
Как быстро поднять корень сразу для тысяч элементов?
Одним запросом вытащите пары «ссылка — родитель» для всего справочника, положите в соответствие и досчитайте корни в памяти. Это одно обращение к базе вместо тысяч.