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

Программист 1С v8.3 (Обычные формы) Управленческий учет
← На главную

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

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

Решение 1: Программная рекурсия (поиск в глубину)

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

Суть алгоритма заключается в том, что мы начинаем обход от текущего узла и идем «вниз» по связям. Если в процессе обхода мы снова встречаем исходный элемент — цикл найден.


Функция ПроверитьЗацикленность(Элемент, ТекущийЭлемент = Неопределено) Экспорт
    Если ТекущийЭлемент = Неопределено Тогда
        ТекущийЭлемент = Элемент;
    КонецЕсли;

    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |	ПодчиненныеСтатьи.ПодчиненнаяСтатья КАК Ссылка
    |ИЗ
    |	Справочник.СтатьиФП.СоставСтатьи КАК ПодчиненныеСтатьи
    |ГДЕ
    |	ПодчиненныеСтатьи.Ссылка = &ТекущийЭлемент";

    Запрос.УстановитьПараметр("ТекущийЭлемент", ТекущийЭлемент);
    Выборка = Запрос.Выполнить().Выбрать();

    Пока Выборка.Следующий() Цикл
        // Если нашли ссылку на исходный элемент — это цикл
        Если Выборка.Ссылка = Элемент Тогда
            Возврат Истина;
        КонецЕсли;

        // Рекурсивный вызов для проверки следующего уровня
        Если ПроверитьЗацикленность(Элемент, Выборка.Ссылка) Тогда
            Возврат Истина;
        КонецЕсли;
    КонецЦикла;

    Возврат Ложь;
КонецФункции

Важный нюанс: при использовании этого метода следует помнить о системном ограничении на глубину рекурсии в 1С. Если ваша структура может иметь сотни уровней вложенности, этот код может вызвать аварийное завершение работы сеанса.

Решение 2: Итерационный подход через временные таблицы в запросе

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

Рассмотрим логику построения такого запроса. Мы создаем временную таблицу с базовыми связями, а затем последовательно присоединяем к ней ту же таблицу, «продлевая» цепочки связей.


// Пример динамического формирования текста запроса для обхода уровней
ТекстЗапросаИтерация = 
"ВЫБРАТЬ
|	ТЗ_Уровень_Пред.Верхняя КАК Верхняя,
|	ТЗ_Зависимости.ПодчиненнаяСтатья КАК ПодчиненнаяСтатья,
|	ТЗ_Уровень_Пред.Вес + 1 КАК Вес
|ПОМЕСТИТЬ ТЗ_Уровень_Тек
|ИЗ
|	ТЗ_Зависимости КАК ТЗ_Зависимости
|ВНУТРЕННЕЕ СОЕДИНЕНИЕ ТЗ_Уровень_Пред КАК ТЗ_Уровень_Пред
|	ПО ТЗ_Уровень_Пред.ПодчиненнаяСтатья = ТЗ_Зависимости.Верхняя";

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

Ниже представлен более продвинутый вариант с использованием МенеджерВременныхТаблиц, который позволяет оптимизировать проверку даже для больших объемов данных:


Процедура ПроверкаВложенности(МенеджерВТ)
    Запрос = Новый Запрос;
    Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
    
    // Текст запроса выбирает пары Мать-Дочь и проверяет их равенство
    Запрос.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |	ВтИерархияТек.Дочь = ВтИерархия.Мать КАК Зациклен,
    |	ВЫБОР КОГДА ВтИерархияТек.Дочь = ВтИерархия.Мать 
    |		ТОГДА ВтИерархияТек.Мать ИНАЧЕ ВтИерархия.Мать КОНЕЦ КАК Мать,
    |	ВтИерархияТек.Дочь КАК Дочь
    |ПОМЕСТИТЬ ВтИерархияНовая
    |ИЗ
    |	ВтИерархияПред КАК ВтИерархияТек
    |ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВтИерархия0 КАК ВтИерархия
    |	ПО ВтИерархияТек.Мать = ВтИерархия.Дочь";
    
    // ... логика повторения запроса в цикле до достижения пустого результата
КонецПроцедуры

Решение 3: Архитектурный подход (Closure Table)

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

Для этого создадим Регистр сведений со следующей структурой:

  1. Измерение: Узел (Ссылка на справочник)
  2. Измерение: Потомок (Ссылка на справочник)
  3. Ресурс: Расстояние (Число)

При записи элемента справочника мы программно рассчитываем все его связи и записываем их в этот регистр. Если элемент А ссылается на Б, а Б на С, в регистре будут записи:

Преимущества подхода:

  1. Максимальная глубина вложенности для элемента А получается одним простым запросом: ВЫБРАТЬ МАКСИМУМ(Расстояние) ГДЕ Узел = &А.
  2. Проверка на зацикливание при добавлении связи А -> Б сводится к запросу: «А есть ли уже в регистре запись, где Узел = Б, а Потомок = А?». Если есть — добавление новой связи создаст цикл.

Решение 4: Современный метод (Рекурсивные CTE)

Начиная с версии платформы 8.3.24, в языке запросов 1С появилась поддержка рекурсивных запросов. Это «свежая идея», которую искал автор темы. Теперь не нужно строить циклы в коде Пока ... Цикл для выполнения запросов.

С помощью конструкции ВЫБРАТЬ ... ИЗ ... ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ... ИЗ ... СОЕДИНЕНИЕ ... внутри описания временной таблицы можно построить весь граф связей одним вызовом. В рекурсивной части запроса можно накапливать «путь» в виде строки. Если текущий идентификатор элемента уже встречается в накопленной строке пути — это сигнал о зацикливании.

Проанализируем пример логики такого запроса:


ВЫБРАТЬ
    Ссылка,
    ПодчиненнаяСтатья,
    1 КАК Уровень
ПОМЕСТИТЬ ВТ_Рекурсия
ИЗ
    Справочник.СтатьиФП.СоставСтатьи
    
ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Основание.Ссылка,
    Связи.ПодчиненнаяСтатья,
    Основание.Уровень + 1
ИЗ
    ВТ_Рекурсия КАК Основание
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.СтатьиФП.СоставСтатьи КАК Связи
    ПО Основание.ПодчиненнаяСтатья = Связи.Ссылка

Подведем итоги и дадим рекомендации

Выбор метода зависит от ваших условий эксплуатации системы:

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

← На главную