В практике разработчика 1С часто возникает задача преобразования данных из плоской структуры (например, обычной табличной части на форме) в иерархическую для последующей записи в Регистр Сведений. Типичный пример: производственные операции, где одна операция может включать в себя несколько подчиненных, а те, в свою очередь, имеют собственные вложенные подзадачи. В этой статье мы подробно разберем, как реализовать такой механизм, проанализируем различные алгоритмические подходы и выясним, как избежать типичных ошибок, таких как бесконечные циклы при зацикливании связей.
Представим, что у нас есть таблица, в которой указаны пары связей: «Основная операция» и «Подчиненная операция». Нам необходимо построить полную цепочку зависимостей для каждой корневой операции и зафиксировать эти связи в регистре. Проблема заключается в том, что глубина вложенности заранее неизвестна, а стандартный язык запросов 1С не поддерживает рекурсивные выражения напрямую. Рассмотрим пошагово, как можно решить эту задачу программно.
Самый очевидный способ обхода древовидных структур — это рекурсия. Мы создаем процедуру, которая вызывает сама себя для каждого найденного подчиненного элемента. Давайте разберем пример реализации, где мы считываем данные из ТабличнаяЧасть и записываем их в РегистрСведений.
Важный нюанс: при использовании рекурсии необходимо всегда помнить о лимите стека вызовов (в 1С это около 1000 итераций) и риске зацикливания данных. Если операция А ссылается на Б, а Б — на А, программа зависнет.
Процедура ЗаписатьСвязиРекурсивно(РодительСсылка, ТаблицаСвязей, СписокПосещенных)
// Проверка на зацикливание: если узел уже обрабатывался в этой ветке, выходим
Если СписокПосещенных.НайтиПоЗначению(РодительСсылка) <> Неопределено Тогда
Возврат;
КонецЕсли;
СписокПосещенных.Добавить(РодительСсылка);
// Ищем все строки, где текущий элемент является родителем
СтрокиДети = ТаблицаСвязей.НайтиСтроки(Новый Структура("Родитель", РодительСсылка));
Для Каждого СтрокаРебенок Из СтрокиДети Цикл
// Записываем связь в Регистр Сведений
МенеджерЗаписи = РегистрыСведений.СвязиОпераций.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ВедущаяОперация = РодительСсылка;
МенеджерЗаписи.ПодчиненнаяОперация = СтрокаРебенок.Операция;
МенеджерЗаписи.Записать();
// Рекурсивный вызов для поиска детей следующего уровня
ЗаписатьСвязиРекурсивно(СтрокаРебенок.Операция, ТаблицаСвязей, СписокПосещенных);
КонецЦикла;
// Удаляем из посещенных при выходе из ветки, чтобы узел был доступен для других веток
СписокПосещенных.Удалить(СписокПосещенных.Количество() - 1);
КонецПроцедуры
Если структура данных очень ветвистая или глубокая, рекурсия может быть опасна. Разберем альтернативный метод — использование массива в качестве «Очереди задач». Этот метод линейный и не нагружает стек вызовов. Мы помещаем в Массив корневой элемент и в цикле Пока обрабатываем его, добавляя новых найденных потомков в конец того же массива.
Проанализируем алгоритм работы с очередью:
МассивОчередь.
Процедура ОбработатьОчередьСвязей(КорневаяОперация, ТаблицаДанных)
Очередь = Новый Массив;
Очередь.Добавить(КорневаяОперация);
Индекс = 0;
Пока Индекс < Очередь.Количество() Цикл
ТекущийРодитель = Очередь[Индекс];
Индекс = Индекс + 1;
Отбор = Новый Структура("Владелец", ТекущийРодитель);
НайденныеСтроки = ТаблицаДанных.НайтиСтроки(Отбор);
Для Каждого Строка Из НайденныеСтроки Цикл
// Фиксируем связь
НоваяЗапись = РегистрыСведений.СвязиОпераций.СоздатьМенеджерЗаписи();
НоваяЗапись.ГлавныйУзел = КорневаяОперация;
НоваяЗапись.ТекущийУзел = Строка.Подчиненный;
НоваяЗапись.Записать();
// Добавляем в очередь для проверки вложенности
Очередь.Добавить(Строка.Подчиненный);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Иногда эффективнее работать не с объектами, а с временными таблицами в запросе. Поскольку в 1С:QL нет рекурсивных CTE, мы можем использовать цикл в коде 1С, внутри которого выполняется Запрос. На каждой итерации мы ищем элементы, у которых «Родитель» находится в списке результатов предыдущей итерации.
Рассмотрим, как это реализовать:
Этот метод значительно быстрее на больших объемах данных, так как основная работа происходит в СУБД, а не на стороне сервера приложений 1С.
Мало кто знает, что механизм Связи наборов данных в СКД умеет самостоятельно обрабатывать иерархию любой глубины. Если вам нужно просто вывести результат или подготовить данные для программной обработки, выясним, как настроить СКД:
Родитель, в поле «Приемник» — Идентификатор.После этого СКД сама выполнит рекурсивный перебор и построит дерево, которое можно выгрузить в ДеревоЗначений одним методом ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений.
Для оптимизации последующего чтения данных из регистра (чтобы не вычислять связи каждый раз заново), мы рекомендуем использовать структуру «транзитивного замыкания». Это означает, что в Регистр Сведений записываются не только связи типа «родитель-сын», но и все связи «предок-потомок». Например, для цепочки 14 -> 15 -> 20 в регистре должны быть записи: (14,15), (15,20) и (14,20). Это позволит в будущем получить список абсолютно всех подчиненных элементов для узла 14 простым запросом без использования циклов и рекурсий.
Резюмируя: выбор метода зависит от объема данных. Для небольших табличных частей управляемой формы вполне достаточно рекурсии с проверкой на зацикливание. Для высоконагруженных систем с тысячами строк лучше использовать итерационный подход с очередью или запросы во временных таблицах с индексацией полей связи.