Как правильно скопировать папку справочника со всеми вложенными группами и элементами в 1С

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

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

Почему простой обход со списком значений или поиск по наименованию не работают

Проанализируем распространенные подходы, которые приводят к ошибкам при копировании разветвленной структуры:

  1. Использование списка значений как стека: если сохранять созданные группы в список значений и брать последнего родителя через обращение по последнему индексу, структура нарушается. Как только в одной ветке заканчивается обход подгруппы и алгоритм переходит к параллельной группе того же уровня, элементы ошибочно попадают в последнюю созданную подгруппу, так как из списка не извлекаются отработанные родители.
  2. Неправильная настройка выборки запроса: вызов метода Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией) требует обязательного наличия секции ИТОГИ ПО ИЕРАРХИЯ в тексте запроса. Без нее результат обходится как плоский список.
  3. Поиск родителя по наименованию: метод НайтиПоНаименованию не гарантирует корректность, если в разных ветках справочника встречаются группы с одинаковыми именами, либо если длина наименования превышает допустимый лимит поиска.

Способ 1. Копирование с использованием объекта «Соответствие» (рекомендуемый)

Рассмотрим наиболее производительный и универсальный способ. Идея заключается в сохранении связки «ИсходнаяСсылка — НоваяСсылка» в объекте Соответствие. Это позволяет мгновенно определять нового родителя для каждого следующего объекта.

Разберем по шагам логику работы алгоритма:

  1. Формируем запрос, выбирающий все элементы и группы, подчиненные исходной папке.
  2. Сортируем выборку по иерархии или уровню вложенности, чтобы родительские папки гарантированно создавались раньше входящих в них элементов.
  3. Перед началом цикла сопоставляем исходную корневую папку с новой папкой-приемником.
  4. При копировании каждого объекта устанавливаем нового родителя из соответствия и, если скопированный объект является группой, добавляем его в соответствие.

Посмотрим на пример программной реализации:


Процедура СкопироватьВеткуСправочника(ПапкаИсточник, ПапкаПриемник)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Спр.Ссылка КАК Ссылка,
        |   Спр.ЭтоГруппа КАК ЭтоГруппа
        |ИЗ
        |   Справочник.Номенклатура КАК Спр
        |ГДЕ
        |   Спр.Ссылка В ИЕРАРХИИ(&ПапкаИсточник)
        |   И Спр.Ссылка <> &ПапкаИсточник
        |УПОРЯДОЧИТЬ ПО
        |   Спр.ЭтоГруппа ИЕРАРХИЯ";
        
    Запрос.УстановитьПараметр("ПапкаИсточник", ПапкаИсточник);
    Выборка = Запрос.Выполнить().Выбрать();
    
    // Хранилище связей старых ссылок с новыми
    СоответствиеРодителей = Новый Соответствие;
    СоответствиеРодителей.Вставить(ПапкаИсточник, ПапкаПриемник);
    
    НачатьТранзакцию();
    Попытка
        Пока Выборка.Следующий() Цикл
            ТекущийОбъект = Выборка.Ссылка.ПолучитьОбъект();
            НовыйОбъект = ТекущийОбъект.Скопировать();
            
            // Находим новую родительскую группу по соответствию
            НовыйРодитель = СоответствиеРодителей[ТекущийОбъект.Родитель];
            Если НовыйРодитель = Неопределено Тогда
                НовыйРодитель = ПапкаПриемник;
            КонецЕсли;
            
            НовыйОбъект.Родитель = НовыйРодитель;
            
            // Очищаем код, если требуется автоматическая генерация нового
            НовыйОбъект.Код = "";
            
            // Ускоряем запись, пропуская лишние проверки при необходимости
            НовыйОбъект.ОбменДанными.Загрузка = Истина;
            НовыйОбъект.Записать();
            
            // Если скопирована группа — фиксируем ее соответствие
            Если Выборка.ЭтоГруппа Тогда
                СоответствиеРодителей.Вставить(Выборка.Ссылка, НовыйОбъект.Ссылка);
            КонецЕсли;
        КонецЦикла;
        
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ВызватьИсключение;
    КонецПопытки;
    
КонецПроцедуры

Способ 2. Рекурсивный обход иерархии

Рассмотрим альтернативный классический подход с использованием рекурсии. Данный метод удобен тем, что перенос выполняется строго по уровням дерева, однако требует внимательности при контроле глубины стека вызовов.

Разберем процедуру, которая принимает исходную группу и целевую группу, копируя сначала элементы текущего уровня, а затем рекурсивно вызывая себя для каждой подгруппы:


Процедура СкопироватьПодчиненныеРекурсивно(ГруппаИсточник, ГруппаПриемник)
    
    // 1. Копируем все элементы (не группы) текущего уровня
    ЗапросЭлементы = Новый Запрос;
    ЗапросЭлементы.Текст = 
        "ВЫБРАТЬ
        |   Спр.Ссылка КАК Ссылка
        |ИЗ
        |   Справочник.Номенклатура КАК Спр
        |ГДЕ
        |   Спр.Родитель = &Родитель
        |   И Спр.ЭтоГруппа = ЛОЖЬ";
    ЗапросЭлементы.УстановитьПараметр("Родитель", ГруппаИсточник);
    
    ВыборкаЭлементов = ЗапросЭлементы.Выполнить().Выбрать();
    Пока ВыборкаЭлементов.Следующий() Цикл
        ОбъектЭлемент = ВыборкаЭлементов.Ссылка.ПолучитьОбъект();
        НовыйЭлемент = ОбъектЭлемент.Скопировать();
        НовыйЭлемент.Родитель = ГруппаПриемник;
        НовыйЭлемент.Код = "";
        НовыйЭлемент.Записать();
    КонецЦикла;
    
    // 2. Находим подгруппы текущего уровня
    ЗапросГруппы = Новый Запрос;
    ЗапросГруппы.Текст = 
        "ВЫБРАТЬ
        |   Спр.Ссылка КАК Ссылка
        |ИЗ
        |   Справочник.Номенклатура КАК Спр
        |ГДЕ
        |   Спр.Родитель = &Родитель
        |   И Спр.ЭтоГруппа = ИСТИНА";
    ЗапросГруппы.УстановитьПараметр("Родитель", ГруппаИсточник);
    
    ВыборкаГрупп = ЗапросГруппы.Выполнить().Выбрать();
    Пока ВыборкаГрупп.Следующий() Цикл
        ОбъектГруппа = ВыборкаГрупп.Ссылка.ПолучитьОбъект();
        НоваяГруппа = ОбъектГруппа.Скопировать();
        НоваяГруппа.Родитель = ГруппаПриемник;
        НоваяГруппа.Код = "";
        НоваяГруппа.Записать();
        
        // Рекурсивно переносим содержимое найденной группы
        СкопироватьПодчиненныеРекурсивно(ВыборкаГрупп.Ссылка, НоваяГруппа.Ссылка);
    КонецЦикла;
    
КонецПроцедуры

Важные нюансы при копировании справочников

Проанализируем технические детали, которые необходимо учитывать при реализации логики копирования:

  1. Уникальность кодов: при вызове метода Скопировать() код исходного объекта переносится в новый. Если в справочнике включен контроль уникальности кодов и автонумерация, это приведет к ошибке при записи. Обязательно очищайте свойство Код перед вызовом Записать().
  2. Подчиненные данные и регистры: метод Скопировать() дублирует только реквизиты и табличные части объекта. Записи независимых или подчиненных регистров сведений (например, цены номенклатуры, штрихкоды), а также элементы подчиненных справочников (единицы измерения, файлы) автоматически не копируются. Их необходимо переносить отдельными запросами с подменой ссылки владельца.
  3. Использование транзакций: при копировании больших массивов данных оборачивайте цикл в НачатьТранзакцию() и ЗафиксироватьТранзакцию(). Это защитит базу от появления частично скопированных деревьев при возникновении ошибок и существенно ускорит запись на диск.
← На главную