В практике разработки на платформе 1С:Предприятие 8 часто встречаются задачи, когда стандартной иерархии справочника недостаточно. Типичный пример — управленческая отчетность или сложные спецификации номенклатуры, где элементы ссылаются друг на друга через табличные части, образуя не дерево, а ориентированный граф. В таких структурах возникают две критические проблемы: как вычислить максимальную глубину вложений и как не допустить «зацикливания» (когда элемент А ссылается на Б, а Б — на А).
Разберем подробно несколько подходов к решению этих задач: от простых рекурсивных функций до сложных оптимизированных запросов и современных возможностей платформы.
Проанализируем самый очевидный способ — использование рекурсивной функции в коде 1С. Этот метод хорошо подходит для проверки конкретного элемента в момент его записи. Рассмотрим пример функции, которая проверяет, не приведет ли новая связь к зацикливанию.
Суть алгоритма заключается в том, что мы начинаем обход от текущего узла и идем «вниз» по связям. Если в процессе обхода мы снова встречаем исходный элемент — цикл найден.
Функция ПроверитьЗацикленность(Элемент, ТекущийЭлемент = Неопределено) Экспорт
Если ТекущийЭлемент = Неопределено Тогда
ТекущийЭлемент = Элемент;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПодчиненныеСтатьи.ПодчиненнаяСтатья КАК Ссылка
|ИЗ
| Справочник.СтатьиФП.СоставСтатьи КАК ПодчиненныеСтатьи
|ГДЕ
| ПодчиненныеСтатьи.Ссылка = &ТекущийЭлемент";
Запрос.УстановитьПараметр("ТекущийЭлемент", ТекущийЭлемент);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Если нашли ссылку на исходный элемент — это цикл
Если Выборка.Ссылка = Элемент Тогда
Возврат Истина;
КонецЕсли;
// Рекурсивный вызов для проверки следующего уровня
Если ПроверитьЗацикленность(Элемент, Выборка.Ссылка) Тогда
Возврат Истина;
КонецЕсли;
КонецЦикла;
Возврат Ложь;
КонецФункции
Важный нюанс: при использовании этого метода следует помнить о системном ограничении на глубину рекурсии в 1С. Если ваша структура может иметь сотни уровней вложенности, этот код может вызвать аварийное завершение работы сеанса.
Выясним, как решить задачу более эффективно, минимизируя количество обращений к базе данных. Вместо множества мелких запросов в цикле можно использовать один большой запрос, текст которого формируется динамически. Этот метод позволяет за один проход определить связи на определенную глубину.
Рассмотрим логику построения такого запроса. Мы создаем временную таблицу с базовыми связями, а затем последовательно присоединяем к ней ту же таблицу, «продлевая» цепочки связей.
// Пример динамического формирования текста запроса для обхода уровней
ТекстЗапросаИтерация =
"ВЫБРАТЬ
| ТЗ_Уровень_Пред.Верхняя КАК Верхняя,
| ТЗ_Зависимости.ПодчиненнаяСтатья КАК ПодчиненнаяСтатья,
| ТЗ_Уровень_Пред.Вес + 1 КАК Вес
|ПОМЕСТИТЬ ТЗ_Уровень_Тек
|ИЗ
| ТЗ_Зависимости КАК ТЗ_Зависимости
|ВНУТРЕННЕЕ СОЕДИНЕНИЕ ТЗ_Уровень_Пред КАК ТЗ_Уровень_Пред
| ПО ТЗ_Уровень_Пред.ПодчиненнаяСтатья = ТЗ_Зависимости.Верхняя";
Проанализируем ситуацию: если на очередной итерации запрос возвращает пустой результат (нет новых связей), значит, мы достигли максимальной глубины. Количество итераций и будет равно уровню вложенности. Чтобы поймать цикл, мы можем проверять, не совпадает ли Верхняя и ПодчиненнаяСтатья на каждом шаге.
Ниже представлен более продвинутый вариант с использованием МенеджерВременныхТаблиц, который позволяет оптимизировать проверку даже для больших объемов данных:
Процедура ПроверкаВложенности(МенеджерВТ)
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// Текст запроса выбирает пары Мать-Дочь и проверяет их равенство
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ВтИерархияТек.Дочь = ВтИерархия.Мать КАК Зациклен,
| ВЫБОР КОГДА ВтИерархияТек.Дочь = ВтИерархия.Мать
| ТОГДА ВтИерархияТек.Мать ИНАЧЕ ВтИерархия.Мать КОНЕЦ КАК Мать,
| ВтИерархияТек.Дочь КАК Дочь
|ПОМЕСТИТЬ ВтИерархияНовая
|ИЗ
| ВтИерархияПред КАК ВтИерархияТек
|ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВтИерархия0 КАК ВтИерархия
| ПО ВтИерархияТек.Мать = ВтИерархия.Дочь";
// ... логика повторения запроса в цикле до достижения пустого результата
КонецПроцедуры
Рассмотрим вариант, который кардинально ускоряет чтение данных ценой некоторого замедления записи. Если ваша задача — мгновенно получать максимальную глубину вложенности в отчетах, стоит использовать Таблицу замыканий (Closure Table).
Для этого создадим Регистр сведений со следующей структурой:
Узел (Ссылка на справочник)Потомок (Ссылка на справочник)Расстояние (Число)При записи элемента справочника мы программно рассчитываем все его связи и записываем их в этот регистр. Если элемент А ссылается на Б, а Б на С, в регистре будут записи:
Преимущества подхода:
ВЫБРАТЬ МАКСИМУМ(Расстояние) ГДЕ Узел = &А.Начиная с версии платформы 8.3.24, в языке запросов 1С появилась поддержка рекурсивных запросов. Это «свежая идея», которую искал автор темы. Теперь не нужно строить циклы в коде Пока ... Цикл для выполнения запросов.
С помощью конструкции ВЫБРАТЬ ... ИЗ ... ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ... ИЗ ... СОЕДИНЕНИЕ ... внутри описания временной таблицы можно построить весь граф связей одним вызовом. В рекурсивной части запроса можно накапливать «путь» в виде строки. Если текущий идентификатор элемента уже встречается в накопленной строке пути — это сигнал о зацикливании.
Проанализируем пример логики такого запроса:
ВЫБРАТЬ
Ссылка,
ПодчиненнаяСтатья,
1 КАК Уровень
ПОМЕСТИТЬ ВТ_Рекурсия
ИЗ
Справочник.СтатьиФП.СоставСтатьи
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Основание.Ссылка,
Связи.ПодчиненнаяСтатья,
Основание.Уровень + 1
ИЗ
ВТ_Рекурсия КАК Основание
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.СтатьиФП.СоставСтатьи КАК Связи
ПО Основание.ПодчиненнаяСтатья = Связи.Ссылка
Выбор метода зависит от ваших условий эксплуатации системы:
Совет: Чтобы «очумелые ручки» не могли обойти проверку при записи (например, через групповое изменение реквизитов или обмены), всегда дублируйте проверку зацикленности в подписке на событие ПередЗаписью и дополнительно создайте регламентное задание, которое раз в сутки проверяет целостность всего справочника.