Как найти и выгрузить иерархические связи из табличной части в регистр сведений 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

Постановка задачи и анализ структуры данных

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

Решение 1: Использование классической рекурсии

Самый очевидный способ обхода древовидных структур — это рекурсия. Мы создаем процедуру, которая вызывает сама себя для каждого найденного подчиненного элемента. Давайте разберем пример реализации, где мы считываем данные из ТабличнаяЧасть и записываем их в РегистрСведений.

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


Процедура ЗаписатьСвязиРекурсивно(РодительСсылка, ТаблицаСвязей, СписокПосещенных)
    // Проверка на зацикливание: если узел уже обрабатывался в этой ветке, выходим
    Если СписокПосещенных.НайтиПоЗначению(РодительСсылка) <> Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    СписокПосещенных.Добавить(РодительСсылка);
    
    // Ищем все строки, где текущий элемент является родителем
    СтрокиДети = ТаблицаСвязей.НайтиСтроки(Новый Структура("Родитель", РодительСсылка));
    
    Для Каждого СтрокаРебенок Из СтрокиДети Цикл
        // Записываем связь в Регистр Сведений
        МенеджерЗаписи = РегистрыСведений.СвязиОпераций.СоздатьМенеджерЗаписи();
        МенеджерЗаписи.ВедущаяОперация = РодительСсылка;
        МенеджерЗаписи.ПодчиненнаяОперация = СтрокаРебенок.Операция;
        МенеджерЗаписи.Записать();
        
        // Рекурсивный вызов для поиска детей следующего уровня
        ЗаписатьСвязиРекурсивно(СтрокаРебенок.Операция, ТаблицаСвязей, СписокПосещенных);
    КонецЦикла;
    
    // Удаляем из посещенных при выходе из ветки, чтобы узел был доступен для других веток
    СписокПосещенных.Удалить(СписокПосещенных.Количество() - 1);
КонецПроцедуры

Решение 2: Обход через очередь (итерационный метод)

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

Проанализируем алгоритм работы с очередью:

  1. Помещаем начальный идентификатор операции в МассивОчередь.
  2. Пока в очереди есть элементы, извлекаем первый.
  3. Находим всех его прямых потомков в исходной таблице.
  4. Записываем связи в регистр и добавляем найденных потомков в очередь для дальнейшего анализа.

Процедура ОбработатьОчередьСвязей(КорневаяОперация, ТаблицаДанных)
    Очередь = Новый Массив;
    Очередь.Добавить(КорневаяОперация);
    
    Индекс = 0;
    Пока Индекс < Очередь.Количество() Цикл
        ТекущийРодитель = Очередь[Индекс];
        Индекс = Индекс + 1;
        
        Отбор = Новый Структура("Владелец", ТекущийРодитель);
        НайденныеСтроки = ТаблицаДанных.НайтиСтроки(Отбор);
        
        Для Каждого Строка Из НайденныеСтроки Цикл
            // Фиксируем связь
            НоваяЗапись = РегистрыСведений.СвязиОпераций.СоздатьМенеджерЗаписи();
            НоваяЗапись.ГлавныйУзел = КорневаяОперация;
            НоваяЗапись.ТекущийУзел = Строка.Подчиненный;
            НоваяЗапись.Записать();
            
            // Добавляем в очередь для проверки вложенности
            Очередь.Добавить(Строка.Подчиненный);
        КонецЦикла;
    КонецЦикла;
КонецПроцедуры

Решение 3: Метод «Черпака» в запросах

Иногда эффективнее работать не с объектами, а с временными таблицами в запросе. Поскольку в 1С:QL нет рекурсивных CTE, мы можем использовать цикл в коде 1С, внутри которого выполняется Запрос. На каждой итерации мы ищем элементы, у которых «Родитель» находится в списке результатов предыдущей итерации.

Рассмотрим, как это реализовать:

Этот метод значительно быстрее на больших объемах данных, так как основная работа происходит в СУБД, а не на стороне сервера приложений 1С.

Решение 4: Использование Системы компоновки данных (СКД)

Мало кто знает, что механизм Связи наборов данных в СКД умеет самостоятельно обрабатывать иерархию любой глубины. Если вам нужно просто вывести результат или подготовить данные для программной обработки, выясним, как настроить СКД:

  1. Создаем набор данных — запрос к нашей таблице.
  2. В закладке «Связи наборов данных» связываем этот набор сам с собой.
  3. В поле «Источник» указываем поле Родитель, в поле «Приемник» — Идентификатор.
  4. Устанавливаем галочку Иерархическая связь.

После этого СКД сама выполнит рекурсивный перебор и построит дерево, которое можно выгрузить в ДеревоЗначений одним методом ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений.

Транзитивное замыкание графа

Для оптимизации последующего чтения данных из регистра (чтобы не вычислять связи каждый раз заново), мы рекомендуем использовать структуру «транзитивного замыкания». Это означает, что в Регистр Сведений записываются не только связи типа «родитель-сын», но и все связи «предок-потомок». Например, для цепочки 14 -> 15 -> 20 в регистре должны быть записи: (14,15), (15,20) и (14,20). Это позволит в будущем получить список абсолютно всех подчиненных элементов для узла 14 простым запросом без использования циклов и рекурсий.

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

← На главную