При интеграции систем или первоначальном заполнении баз данных программисты 1С часто сталкиваются с необходимостью импорта иерархических структур. Это могут быть как классические файлы Excel с вложенными каталогами товаров, так и предварительно выгруженные во временные таблицы деревья значений. Главная сложность здесь заключается в том, чтобы перенести эту структуру в иерархический справочник 1С, полностью сохранив подчиненность "родитель — потомок", и при этом не создать дублирующиеся записи.
В этой статье мы подробно разберем, как решить эту задачу с помощью классического программирования в среде 1С:Предприятие. Мы проанализируем базовые подходы, найдем скрытые архитектурные ловушки, которые часто упускают начинающие разработчики, и напишем надежный, быстроработающий программный код.
На тематических форумах часто предлагают простой шаблон рекурсивного обхода дерева значений, который выглядит следующим образом:
Процедура Сформировать(Строки)
Для Каждого Строка Из Строки Цикл
СоздатьЭлементСправочника(Строка)
Сформировать(Строка.Строки);
КонецЦикла;
КонецПроцедуры
Внимание! Использование данного шаблона в неизмененном виде приведет к полной потере иерархии в вашей базе данных. Давайте проанализируем ситуацию и выясним причину. В этой процедуре на следующий уровень рекурсии передается только коллекция дочерних строк Строка.Строки. Однако ссылка на только что созданный родительский элемент (или группу) никуда не передается и не сохраняется для подчиненных элементов. В результате выполнения такого кода абсолютно все созданные группы и элементы запишутся на самый верхний (корневой) уровень справочника.
Чтобы иерархия сохранялась, мы должны обязательно передавать ссылку на созданного родителя на каждый последующий уровень рекурсивного вызова.
Для создания правильного механизма загрузки нам необходимо выполнить следующие шаги:
Рассмотрим подробнее каждый из этих этапов.
В иерархических справочниках 1С критически важно разделять создание групп и элементов. Для этого перед написанием кода обязательно загляните в Конфигуратор и проверьте свойства вашего справочника. Вкладка "Иерархия" содержит настройку "Вид иерархии":
Справочники.[ИмяСправочника].СоздатьГруппу(), а для элемента — Справочники.[ИмяСправочника].СоздатьЭлемент().СоздатьЭлемент().Как определить, что текущая строка дерева является группой? Мы можем сделать это по наличию подчиненных строк. Если у текущей строки дерева количество дочерних элементов больше нуля, значит, это группа:
ЭтоГруппа = (СтрокаДерева.Строки.Количество() > 0);
Чтобы избежать дублирования информации при повторных запусках обработки, мы должны сначала искать объект в базе данных.
Для групп (папок) поиск по наименованию — самый логичный вариант. Однако здесь есть важный нюанс: группы с одинаковыми названиями могут находиться в разных ветках справочника. Например, папка "Расходные материалы" может быть и в "Офисе", и в "Производстве". Поэтому искать группу нужно строго внутри текущего родительского узла. Для этого мы используем метод НайтиПоНаименованию с указанием родителя:
НайденнаяГруппа = Справочники.Номенклатура.НайтиПоНаименованию(ИмяГруппы, Истина, РодительСсылка);
Для конечных элементов поиск по наименованию часто неэффективен и опасен, так как названия товаров могут частично совпадать. Гораздо надежнее производить поиск по уникальному идентификатору, коду из внешней системы или артикулу:
НайденныйЭлемент = Справочники.Номенклатура. НайтиПоКоду(КодИзФайла);
Давайте объединим все наши правила и соображения в единый работающий алгоритм. Ниже представлен готовый код рекурсивной загрузки дерева значений в справочник "Номенклатура":
// Процедура-инициатор процесса загрузки
Процедура ЗагрузитьДеревоВБазу(ДеревоЗначений)
// Открываем транзакцию. Это ускорит запись в БД в десятки раз
НачатьТранзакцию();
Попытка
// Начинаем обход с самого верхнего уровня.
// В качестве начального родителя передаем пустую ссылку справочника.
ОбходИЗагрузкаДерева(ДеревоЗначений.Строки, Справочники.Номенклатура.ПустаяСсылка());
// Если все прошло успешно, фиксируем изменения в базе данных
ЗафиксироватьТранзакцию();
Сообщить("Данные успешно иерархически загружены в справочник!");
Исключение
// В случае любой ошибки откатываем изменения, чтобы не засорять базу
ОтменитьТранзакцию();
Сообщить("Произошла ошибка при загрузке данных: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
// Рекурсивная процедура для обхода уровней дерева
Процедура ОбходИЗагрузкаДерева(СтрокиДерева, РодительСсылка)
Для Каждого СтрокаДерева Из СтрокиДерева Цикл
ТекущаяСсылка = Неопределено;
// 1. Проверяем, группа это или элемент
ЭтоГруппа = (СтрокаДерева.Строки.Количество() > 0);
Если ЭтоГруппа Тогда
// 2. Обрабатываем Группу
// Ищем существующую группу строго внутри текущего родителя
ТекущаяСсылка = Справочники.Номенклатура.НайтиПоНаименованию(СтрокаДерева.Наименование, Истина, РодительСсылка);
Если ТекущаяСсылка.Пустая() Тогда
// Если группа не найдена, создаем новую
НоваяГруппа = Справочники.Номенклатура.СоздатьГруппу();
НоваяГруппа.Наименование = СтрокаДерева.Наименование;
НоваяГруппа.Родитель = РодительСсылка;
// Записываем группу в базу данных, чтобы получить ссылку
НоваяГруппа.Записать();
ТекущаяСсылка = НоваяГруппа.Ссылка;
КонецЕсли;
Иначе
// 3. Обрабатываем Элемент
// Ищем элемент по уникальному коду, пришедшему из источника
ТекущаяСсылка = Справочники.Номенклатура.НайтиПоКоду(СтрокаДерева.Код);
Если ТекущаяСсылка.Пустая() Тогда
// Создаем новый элемент, если его нет в базе
НовыйЭлемент = Справочники.Номенклатура.СоздатьЭлемент();
НовыйЭлемент.Наименование = СтрокаДерева.Наименование;
НовыйЭлемент.Код = СтрокаДерева.Код;
НовыйЭлемент.Родитель = РодительСсылка; // Привязываем к родителю
// Заполняем дополнительные реквизиты при наличии
// НовыйЭлемент.Артикул = СтрокаДерева.Артикул;
НовыйЭлемент.Записать();
ТекущаяСсылка = НовыйЭлемент.Ссылка;
Иначе
// Опционально: обновляем реквизиты уже существующего элемента
// ЭлементОбъект = ТекущаяСсылка.ПолучитьОбъект();
// ЭлементОбъект.Наименование = СтрокаДерева.Наименование;
// ЭлементОбъект.Родитель = РодительСсылка;
// ЭлементОбъект.Записать();
КонецЕсли;
КонецЕсли;
// 4. Рекурсивный переход на более глубокий уровень
// Если у текущей строки дерева есть дочерние строки, уходим вглубь.
// Передаем коллекцию дочерних строк и ссылку на текущую созданную/найденную группу.
Если СтрокаДерева.Строки.Количество() > 0 Тогда
ОбходИЗагрузкаДерева(СтрокаДерева.Строки, ТекущаяСсылка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Для того чтобы ваша обработка работала стабильно даже на справочниках объемом в сотни тысяч элементов, возьмите на вооружение следующие рекомендации:
НачатьТранзакцию() объединяет все эти обращения в один пакет. Скорость работы алгоритма при этом возрастает в 10–50 раз.Применяя данный рекурсивный метод с корректной передачей контекста родителя, вы сможете безопасно и быстро переносить любые иерархические структуры данных из файлов Excel или внешних API в ваши информационные базы 1С.