Проблема циклических ссылок в базах данных «1С:Предприятие» — это классическая задача, с которой сталкиваются разработчики при проектировании сложных систем, обмене данными или построении иерархических отчетов. Цикл возникает, когда объект «А» ссылается на объект «Б», тот — на «В», а «В», в свою очередь, замыкает цепочку, ссылаясь обратно на «А». В этой статье мы подробно разберем, как теоретически и практически обнаружить такие связи, даже если их глубина заранее неизвестна.
Прежде чем перейти к методам поиска, проанализируем ситуацию, описанную в обсуждении. Основная опасность циклических ссылок заключается в бесконечной рекурсии. Рассмотрим пример из практики: при попытке выгрузить объект, который входит в «кольцо», программный код может начать бесконечно обходить связанные элементы, что в конечном итоге приводит к ошибке Stack Overflow (переполнение стека). Это критично для универсальных механизмов обмена данными, сериализации в JSON или XML, а также для алгоритмов расчета себестоимости или распределения затрат.
Долгое время язык запросов 1С не поддерживал рекурсию, и разработчикам приходилось имитировать её через фиксированное количество соединений. Однако, начиная с версии платформы 8.3.24, появилась официальная поддержка общих табличных выражений (CTE) с ключевым словом РЕКУРСИВНО.
Рассмотрим, как мы можем использовать этот механизм для поиска циклов. Основная идея заключается в том, чтобы на каждом шаге рекурсии накапливать «путь» пройденных ссылок и проверять, не встречается ли текущая ссылка в этом пути.
Пример логики запроса (концептуально), который может обработать процессор схемы запроса:
ВЫБРАТЬ
Спр.Ссылка КАК НачальныйУзел,
Спр.Ссылка КАК ТекущийУзел,
СТРОКА(Спр.Ссылка) КАК Путь
ИЗ
Справочник.Спр1 КАК Спр
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Рекурсия.НачальныйУзел,
СпрПрисоединяемый.Ссылка,
Рекурсия.Путь + " | " + СТРОКА(СпрПрисоединяемый.Ссылка)
ИЗ
Рекурсия КАК Рекурсия
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Спр1 КАК СпрПрисоединяемый
ПО Рекурсия.ТекущийУзел.РеквизитСсылка = СпрПрисоединяемый.Ссылка
ГДЕ
НАЙТИ(Рекурсия.Путь, СТРОКА(СпрПрисоединяемый.Ссылка)) = 0
Проанализируем этот подход: условие в ГДЕ позволяет нам остановить рекурсию, как только мы обнаружим повторение объекта в цепочке. Это наиболее современный и эффективный способ для баз, работающих на актуальных версиях платформы.
Если ваша платформа ниже версии 8.3.24, мы можем применить математический подход, известный как метод удвоения. Как было отмечено в сообщении 10 обсуждения, этот метод позволяет найти цепочку длиной более миллиона элементов всего за 20 итераций запроса (так как 2 в 20-й степени больше 1 000 000).
Разберем алгоритм по шагам:
ВТ_Связи, где храним пары: Объект и НаКогоСсылается. Это связи длиной 1.ВТ_Связи КАК Т1 СОЕДИНЕНИЕ ВТ_Связи КАК Т2 ПО Т1.НаКогоСсылается = Т2.Объект. Теперь у нас есть связи длиной 2.Объект совпало с полем НаКогоСсылается — цикл найден.Этот метод крайне эффективен, так как количество необходимых пакетов запросов минимально, а основная нагрузка ложится на SQL-сервер, который отлично справляется с соединениями индексов.
Как справедливо замечено в сообщении 15, гораздо проще предотвратить появление цикла, чем искать его потом по всей базе. Рассмотрим, как реализовать проверку в модуле объекта справочника:
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
ТекущаяСсылка = ЭтотОбъект.Ссылка;
ПроверяемаяСсылка = ЭтотОбъект.РеквизитСпр1;
Пока ЗначениеЗаполнено(ПроверяемаяСсылка) Цикл
Если ПроверяемаяСсылка = ТекущаяСсылка Тогда
Сообщить("Обнаружена циклическая ссылка!");
Отказ = Истина;
Прервать;
КонецЕсли;
// Переходим к следующему звену цепочки
ПроверяемаяСсылка = ПроверяемаяСсылка.РеквизитСпр1;
КонецЦикла;
КонецПроцедуры
Проанализируем ситуацию: данный код эффективен для простых цепочек в рамках одного типа метаданных. Однако, если цикл сложный (через несколько разных справочников и документов), потребуется более универсальный обход через метаданные или использование критериев отбора.
Если задача стоит не в том, чтобы запретить циклы, а в том, чтобы выгрузить данные без падения системы, выясним причину ошибки Stack Overflow. Она возникает из-за того, что стандартный сериализатор пытается разыменовать каждую ссылку. Чтобы этого избежать, используем настройки платформы:
ЗаписьJSON настройте параметр СериализацияЦиклическихСсылок. Установите его в значение ВариантСериализацииЦиклическихСсылок.ИспользоватьСсылку. В этом случае вместо бесконечного вложения объектов платформа запишет идентификатор уже упомянутого объекта.СериализаторXDTO. Он автоматически отслеживает объекты в рамках одного сеанса сериализации и не допускает зацикливания, подставляя ссылки на уже созданные узлы XML.Если данных очень много, и запросы не справляются, рассмотрим вариант выгрузки связей в массив с последующей обработкой. Для поиска сильно связных компонент (циклов) в теории графов используется алгоритм Тарьяна.
Суть метода заключается в одном проходе по графу (обход в глубину — DFS). Мы помечаем каждый узел временем посещения и храним стек активных узлов. Если при обходе мы встречаем узел, который уже находится в стеке — мы нашли цикл.
Важный момент для 1С: так как встроенный язык имеет ограничение на глубину рекурсии вызова функций, для реализации DFS на миллионах элементов следует использовать итеративный подход с использованием собственного объекта Массив в качестве стека, вместо рекурсивных вызовов процедур.
Подведем итог нашему анализу:
РЕКУРСИВНЫЕ запросы в версии 8.3.24.ПередЗаписью, но помните о производительности при записи «длинных» цепочек.