Как программно изменить реквизиты группы элементов справочника по результатам запроса в 1С 8.3

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

В практике разработки в среде 1С:Предприятие часто возникает задача массового (группового) изменения реквизитов справочников на основе сложного отбора. В качестве критерия отбора обычно выступает отчет или динамический запрос. Например, нам необходимо найти всех «забытых» клиентов (партнеров), по которым давно не было никаких событий (звонков, встреч, писем), и переназначить их на системного пользователя «Общая база».

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

Основные проблемы исходного кода и их причины

Давайте проанализируем ошибки, которые совершил автор темы в процессе написания своей первой обработки, и разберем, как их исправить.

1. Ошибка обращения к выборке справочника вместо выборки запроса

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

ВыборкаДетальныеЗаписи = Справочники.Партнеры.Выбрать;

При таком подходе платформа полностью игнорирует выполненный ранее запрос со всеми его сложными условиями и отборами по датам. Программа начинает последовательно обходить вообще все элементы справочника Партнеры из базы данных. Для решения этой проблемы необходимо получать выборку непосредственно из объекта результата запроса:


Результат = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = Результат.Выбрать();

2. Ошибка «Нельзя изменять поле, содержащее объект данных формы»

Эта ошибка возникает на управляемых формах 1С при попытке использовать переменную с именем Объект внутри серверной процедуры формы:

Объект = ВыборкаДетальныеЗаписи.Партнер.ПолучитьОбъект(); // Вызовет ошибку!

Выясним причину: в управляемом приложении на уровне формы всегда существует зарезервированный основной реквизит с именем Объект (он связан с типом обрабатываемых данных, например, с самой внешней обработкой). Платформа запрещает переопределять этот системный реквизит локальными переменными. Чтобы устранить конфликт имен, достаточно назвать переменную по-другому, например, ТекущийПартнер или ПартнерОбъект.

3. Ошибка «Элемент не выбран!» при вызове ПолучитьОбъект()

Данная исключительная ситуация возникает в тех случаях, когда запрос вернул пустую ссылку в поле Партнер (например, если в документах CRM_Событие по какой-то причине не был заполнен партнер, или если в результат выборки попала папка/группа справочника, у которой нет искомого реквизита). Попытка вызвать метод ПолучитьОбъект() у пустой ссылки или предопределенного элемента другого типа вызывает аварийное завершение работы программы.

Для предотвращения этой ошибки перед получением объекта обязательно нужно делать проверку заполненности ссылки с помощью функции ЗначениеЗаполнено(), а также исключать группы (папки) справочника на уровне запроса с помощью условия ЭтоГруппа = ЛОЖЬ.

4. Проблема низкой производительности (поиск в цикле)

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


ПартнерОбъект.ОсновнойМенеджер = Справочники.Пользователи.НайтиПоНаименованию("ОбщаяБаза");

Рассмотрим, почему это критично. Метод НайтиПоНаименованию при каждом вызове осуществляет полноценное обращение к СУБД. Если ваш запрос вернет 10 000 клиентов, программа выполнит 10 000 лишних запросов к базе данных для поиска одного и того же пользователя! Это приведет к колоссальному падению производительности и зависанию системы. Поиск ссылки на нового менеджера необходимо выполнить строго один раз до начала цикла.

Пошаговое создание правильной обработки

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

Рассмотрим по шагам реализацию серверного метода:

  1. Сначала найдем целевого пользователя (менеджера), которому будут переданы клиенты, и сохраним ссылку на него в локальную переменную. Если пользователь не найден, прекратим выполнение процедуры, чтобы не записать пустое значение.
  2. Подготовим текст запроса. Чтобы действительно найти «забытых» партнеров, мы должны исключить тех, по кому были события позднее указанной даты. Для этого лучше всего использовать левое соединение или подзапрос с условием «НЕ В».
  3. Передадим параметры в запрос (используя правильный формат даты в кавычках или функцию НачалоДня).
  4. Выполним запрос и обойдем результаты в цикле.
  5. Для каждого партнера получим объект базы данных, проверим, отличается ли текущий менеджер от нового (чтобы избежать лишней перезаписи и оптимизировать транзакции), установим новое значение реквизита и запишем объект.

Оптимизированный готовый код для внешней обработки

Ниже представлен готовый листинг серверной процедуры, написанный в соответствии со стандартами разработки «1С:Предприятие»:


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

КонецПроцедуры

Как запустить этот алгоритм по кнопке на форме

Для вызова описанного кода создадим на управляемой форме обработки стандартную команду (кнопку), связанную с клиентской процедурой. Клиентская процедура, в свою очередь, должна вызывать наш серверный контекст:


&НаКлиенте
Процедура ВыполнитьПереносМенеджеров(Команда)
    
    ОчиститьСообщения();
    ПоказатьПредупреждение(, "Запущена процедура переназначения менеджеров. Пожалуйста, подождите...");
    
    // Вызываем серверный метод
    ПерераспределитьКлиентовНаСервере();
    
КонецПроцедуры

Рекомендации по дальнейшей оптимизации

Если в вашей базе данных количество партнеров исчисляется десятками или сотнями тысяч, единовременная запись всех объектов в рамках одной транзакции может привести к длительной блокировке таблиц СУБД для других пользователей. В таком случае рекомендуется записывать данные порциями (пакетами по 500–1000 штук), фиксируя транзакцию после каждого пакета, либо запускать обработку в фоновом задании во внерабочее время.

← На главную