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