Как скопировать или переместить элементы и подгруппы иерархического справочника в 1С:Предприятие 8.3

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

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

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

Анализ причин типичных ошибок в запросах

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

Ошибка 1: «Неверные параметры в ИЕРАРХИИ»

В исходном коде автора темы была предпринята попытка передать параметр в запрос следующим образом:

Запрос.УстановитьПараметр("Источник", Справочники.Контрагенты);

При выполнении запроса с условием В ИЕРАРХИИ(&Источник) платформа выдает ошибку совместимости параметров. Выясним причину: конструкция Справочники.Контрагенты возвращает объект типа СправочникМенеджер.Контрагенты. Менеджер справочника — это программный инструмент для управления объектами метаданных, он не имеет физического представления в таблицах базы данных и не содержит никаких ссылок. Оператор запроса В ИЕРАРХИИ ожидает на вход конкретную ссылку (тип СправочникСсылка), массив таких ссылок или результат вложенного запроса. Передавать туда менеджер справочника категорически нельзя.

Ошибка 2: «Нельзя сравнивать поля неограниченной длины и поля несовместимых типов»

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

Как правильно передавать параметры?

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

Запрос.УстановитьПараметр("ГруппаИсточник", ГруппаИсточник);

Сценарий 1: Перемещение элементов (без создания дублей)

Прежде чем писать сложный код копирования, давайте проанализируем бизнес-требования. Если перед вами стоит задача просто перенести («перебросить») контрагентов из папки «А» в папку «Б» без создания дублирующих записей в базе данных, вам не нужно копировать элементы. Достаточно изменить значение реквизита Родитель у существующих элементов и групп на новую группу-приемник.

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


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

Этот способ идеален, если структура вложенных подгрупп внутри источника отсутствует или ее не требуется сохранять в исходном виде в приемнике. Однако если внутри источника есть разветвленное дерево папок и элементов, и вы хотите полностью продублировать эту структуру в новом месте, нам понадобится совершенно другой подход.

Сценарий 2: Копирование структуры иерархии с помощью «Соответствия» и метода «ВыбратьИерархически()»

Для создания точных копий элементов с сохранением сложной подчиненности (когда папка «Дочерняя» лежит в папке «Родительская», а в ней лежат конечные элементы) стандартный плоский запрос не совсем удобен. Попытки использовать списки значений в качестве стека для хранения истории родителей (как в первом сообщении темы) ненадежны. При параллельных ветках иерархии такой стек быстро теряет актуальность, и элементы начинают записываться в хаотичном порядке.

Разберем красивое и высокоэффективное решение, основанное на совместной работе двух мощных инструментов платформы 1С:

  1. Метод ВыбратьИерархически(). Он осуществляет последовательный обход элементов дерева. Самое важное свойство этого метода — он гарантирует, что любая группа-родитель будет прочитана в цикле раньше, чем любые ее дочерние элементы или вложенные подгруппы.
  2. Объект класса Соответствие. Это высокопроизводительная хэш-таблица формата «ключ-значение». Мы будем использовать её для сопоставления старых групп их новым скопированным копиям. Ключом будет выступать Ссылка старой группы-источника, а значением — Ссылка новой только что созданной группы-приемника.

Пошаговый алгоритм работы решения:

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

Посмотрим на практическую реализацию этого надежного алгоритма:


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

Важные технологические нюансы, о которых нельзя забывать

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

  1. Автоматическое назначение кодов (уникальность). Метод Скопировать() копирует абсолютно все реквизиты, включая Код. Если в конфигураторе для справочника «Контрагенты» включен контроль уникальности кодов, попытка записать копию со старым кодом приведет к транзакционной ошибке записи. Обязательно сбрасывайте код (НовыйОбъект.Код = "") перед выполнением метода Записать(). Платформа 1С самостоятельно сгенерирует новый уникальный код при записи.
  2. Обработчик событий «ПриКопировании». Помните, что при вызове метода Скопировать() внутри модуля объекта справочника срабатывает стандартный обработчик ПриКопировании(ОбъектКопирования). Разработчики вашей конфигурации могли заложить туда логику очистки определенных полей (например, ИНН, КПП, признаков холдинга или договоров). Если эти реквизиты очищаются, но должны быть заполнены в вашей копии, вам придется принудительно переписать их значения обратно из исходной выборки перед записью.
  3. Предопределенные элементы. Если в копируемой ветке окажутся предопределенные элементы (созданные разработчиком в конфигураторе), метод Скопировать() создаст обычный (ординарный) элемент справочника. Он унаследует имя и реквизиты, но потеряет системное свойство «предопределенный» и не будет связан с метаданными.
  4. Использование явных транзакций. Копирование или перемещение элементов справочника — это дискоемкие операции, выполняющие множество последовательных записей в таблицы базы данных. Выполнение таких операций без открытой явной транзакции (НачатьТранзакцию()) заставит СУБД открывать и закрывать транзакцию для каждого отдельного элемента. Это снизит скорость выполнения процедуры в десятки раз и создаст излишнюю нагрузку на сервер. Кроме того, транзакция защищает вашу базу данных от частичного копирования: если на 500-м элементе произойдет сбой, СУБД полностью откатит состояние системы назад, предотвратив появление «мусора».
← На главную