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