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