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