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