Как оптимизировать регламентное задание и настроить регистрацию изменений в RabbitMQ для 1С:УПП 1.3?

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

В крупных корпоративных системах на базе 1С:Управление производственным предприятием (УПП 1.3) часто используются отраслевые или кастомные интеграционные механизмы. Одним из таких популярных решений является интеграция с шиной данных RabbitMQ (например, от компании «Первый БИТ»). При разработке регламентных заданий, которые массово изменяют реквизиты объектов, программисты часто сталкиваются с неочевидными проблемами: падением производительности базы данных, блокировками таблиц и сбоями при регистрации измененных данных в очереди обмена.

В этой статье мы подробно разберем реальный кейс оптимизации регламентного задания, которое должно снимать признак СТ_R_AndD (исследования и разработки) у групп номенклатуры и автоматически регистрировать связанную номенклатуру к обмену в RabbitMQ. Мы пошагово исправим критические архитектурные ошибки исходного кода, ускорим работу алгоритма в десятки раз и обеспечим надежную транзакционную модель.

В чем заключалась исходная проблема?

Перед разработчиком стояла задача: два раза в неделю запускать регламентное задание, которое находит группы номенклатуры в каталоге (ГНК) с истекшим сроком действия статуса "RnD", снимает этот флаг, а затем регистрирует всю подчиненную номенклатуру (НК) к обмену, чтобы изменения улетели во внешние системы через брокер сообщений RabbitMQ.

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

  1. Выполнял тяжелый SQL-запрос заново на каждой итерации цикла.
  2. Пытался зарегистрировать изменения номенклатуры, передавая ссылку вместо объекта, что приводило к ошибке Поле объекта не обнаружено (ДополнительныеСвойства).
  3. Использовал неэффективную перезапись элементов номенклатуры в БД исключительно ради триггера регистрации изменений.
  4. "Глотал" исключения в транзакциях без записи ошибок в журнал регистрации.

Давайте подробно разберем каждую ошибку и найдем оптимальные решения.

---

Шаг 1. Устранение критического снижения производительности (Запрос в цикле)

Давайте обратим внимание на фрагмент исходного кода, который вызвал бурную реакцию экспертов на форуме:


Пока ВыборкаГНК.Следующий цикл
    СправочникОбъект = ВыборкаГНК.ГНК.ПолучитьОбъект;
    СправочникОбъект.СТ_R_AndD = Ложь;
    // ...
    // ОШИБКА: Повторное выполнение всего запроса на каждой итерации!
    ВыборкаНоменклатура = Запрос.Выполнить.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "НК");
    Пока ВыборкаНоменклатура.Следующий Цикл
        НоменклатураОбъект = ВыборкаНоменклатура.Номенклатура.ПолучитьОбъект;
        НоменклатураОбъект.Записать;
    КонецЦикла;
КонецЦикла;

При вызове метода Запрос.Выполнить() внутри цикла система обращается к СУБД (MS SQL Server / PostgreSQL) заново на каждом шаге. Если регламентное задание обрабатывает 500 групп номенклатуры, запрос выполнится 501 раз! Это приводит к колоссальной нагрузке на процессор сервера и дисковую подсистему.

Для исправления этой архитектурной ошибки мы должны выполнить запрос ровно один раз. Чтобы обойти иерархическую структуру «Группа номенклатуры -> Номенклатура», воспользуемся встроенными возможностями обхода результатов запроса по группировкам. Метод Выбрать() нужно вызывать не у самого запроса, а у текущей строки родительской выборки:


РезультатЗапроса = Запрос.Выполнить();
// Выбираем группировку верхнего уровня (ГНК)
ВыборкаГНК = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ГНК");

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

---

Шаг 2. Решение проблемы с регистрацией в RabbitMQ (ДополнительныеСвойства)

При попытке передать номенклатуру в специализированный модуль интеграции БИТ вылетала ошибка:

{ОбщийМодуль.СТ_ВыгрузкаRabbitMQ.Модуль(20)}: Поле объекта не обнаружено (ДополнительныеСвойства)

Проанализируем причину. Код модуля интеграции проверяет структуру ДополнительныеСвойства у источника:


Процедура адаптер_ПриЗаписиЭлементаПриЗаписи(Источник, Отказ) Экспорт
    Если Не Источник.ДополнительныеСвойства.Свойство("НеПередаватьОбъектПоRabbitMQ") Тогда
    ...

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

Следовательно, передавать в интеграционный модуль нужно исключительно объект, предварительно полученный с помощью метода ПолучитьОбъект().


// ОШИБКА: Передача ссылки приведет к падению системы
СТ_ВыгрузкаRabbitMQ.адаптер_ПриЗаписиЭлементаПриЗаписи(ВыборкаНоменклатура.Номенклатура, Отказ);

// ПРАВИЛЬНО: Получаем объект и передаем его в процедуру регистрации RMQ
НоменклатураОбъект = ВыборкаНоменклатура.Номенклатура.ПолучитьОбъект();
СТ_ВыгрузкаRabbitMQ.адаптер_ПриЗаписиЭлементаПриЗаписи(НоменклатураОбъект, Ложь);

---

Шаг 3. Оптимизация записи в базу данных (Избавление от лишней записи)

В исходном коде для отправки изменений в шину выполнялась непосредственная запись каждого элемента номенклатуры в базу данных:

НоменклатураОбъект.Записать();

Давайте подумаем: сама номенклатура физически никак не изменилась (мы сняли флаг только у группы номенклатуры ГНК). Запись неизмененного объекта в базу данных — это невероятно затратная операция. Платформа 1С при вызове метода Записать() производит:

Поскольку логика RabbitMQ в решениях БИТ формирует XML-пакет напрямую из структуры переданного объекта, нам совершенно не обязательно записывать номенклатуру в СУБД. Достаточно получить объект в память, инициализировать его свойства и вызвать процедуру отправки адаптера. Это позволит избежать лишней нагрузки на диски и процессор сервера.


// Оптимальный вариант без перезаписи элемента в БД
НоменклатураОбъект = ВыборкаНоменклатура.Номенклатура.ПолучитьОбъект();
Если НоменклатураОбъект <> Неопределено Тогда
    // Просто регистрируем изменения в шине без выполнения НоменклатураОбъект.Записать()!
    СТ_ВыгрузкаRabbitMQ.адаптер_ПриЗаписиЭлементаПриЗаписи(НоменклатураОбъект, Ложь);
КонецЕсли;

---

Шаг 4. Безопасное управление транзакциями и журналирование ошибок

Использование конструкций НачатьТранзакцию() и Попытка...Исключение в кодовой базе автора содержало опасный паттерн "молчаливого падения" (silent failure):


Исключение
    Успех = Ложь;
КонецПопытки;
Если Успех Тогда
    ЗафиксироватьТранзакцию;
Иначе
    ОтменитьТранзакцию;
КонецЕсли;

Если при записи ГНК или отправке сообщения произойдет системный сбой (например, блокировка базы данных или сетевой таймаут шины), транзакция откатится, но администраторы и разработчики об этом даже не узнают! В журнале регистрации не останется никаких следов. К тому же, в случае возникновения ошибки СУБД внутри транзакции, она переходит в состояние "битой" (doomed transaction), и любые последующие манипуляции до явного вызова отмены могут приводить к непредсказуемым ошибкам платформы.

Давайте реализуем надежный и безопасный шаблон обработки транзакций с обязательным логированием критических ошибок:


НачатьТранзакцию();
Попытка
    СправочникОбъект.Записать();
    
    // ... логика отправки уведомлений ...
    
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    
    // Записываем ошибку в Журнал регистрации для последующего анализа администратором
    ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
    ЗаписьЖурналаРегистрации(
        "РегламентноеЗадание.СнятиеRnD",
        УровеньЖурналаРегистрации.Ошибка,
        ВыборкаГНК.ГНК.Метаданные(),
        ВыборкаГНК.ГНК,
        "Не удалось обработать группу номенклатуры: " + ВыборкаГНК.ГНК + Символы.ПС + ТекстОшибки
    );
КонецПопытки;

---

Шаг 5. Вынесение кэшируемых операций из тела циклов

Еще одна классическая ошибка оптимизации — вызов методов получения констант внутри тела цикла. Даже если используется модуль с повторным использованием значений (БИТ_СТ_ПовторноеИспользованиеЗначений), само обращение к контексту модуля требует времени.

Давайте получим шаблон сообщения и адреса рассылки один раз в самом начале нашей процедуры:


// Получаем константы ОДИН раз перед выполнением циклов
ШаблонСообщения = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("СТ_ШаблонСообщенияСнятиеRndГНК");
АдресаПолучателей = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("АдресаПолучателейДляОповещенияПоИзменениюПараметра_R_AndD");

---

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

Давайте соберем все наши архитектурные улучшения в единый красивый, быстрый и отказоустойчивый алгоритм:


УстановитьПривилегированныйРежим(Истина);

// 1. Предварительно получаем константы
ШаблонСообщения = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("СТ_ШаблонСообщенияСнятиеRndГНК");
АдресаПолучателей = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("АдресаПолучателейДляОповещенияПоИзменениюПараметра_R_AndD");

// 2. Инициализируем и выполняем запрос
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДатаПроверки", ТекущаяДата());
Запрос.Текст = 
    "ВЫБРАТЬ
    |	БИТ_СТ_ГруппыНоменклатурыВКаталоге.Ссылка КАК ГНК,
    |	Номенклатура.БИТ_СТ_ГруппаНоменклатуры.ОтветственныйКонструктор КАК ОтветственныйКонструктор,
    |	Номенклатура.Ссылка КАК Номенклатура
    |ИЗ
    |	Справочник.БИТ_СТ_ГруппыНоменклатурыВКаталоге КАК БИТ_СТ_ГруппыНоменклатурыВКаталоге
    |	ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Номенклатура
    |		ПО (Номенклатура.БИТ_СТ_ГруппаНомеклатурыВКаталоге = БИТ_СТ_ГруппыНоменклатурыВКаталоге.Ссылка)
    |		И (Номенклатура.БИТ_СТ_ГруппаНоменклатуры.ОтветственныйКонструктор <> ЗНАЧЕНИЕ(Справочник.Пользователи.ПустаяСсылка))
    |ГДЕ
    |	НЕ БИТ_СТ_ГруппыНоменклатурыВКаталоге.ПометкаУдаления
    |	И БИТ_СТ_ГруппыНоменклатурыВКаталоге.СТ_R_AndD
    |	И БИТ_СТ_ГруппыНоменклатурыВКаталоге.СТ_ПлановаяДатаСнятия <= &ДатаПроверки
    |ИТОГИ ПО
    |	ГНК,
    |	Номенклатура";

РезультатЗапроса = Запрос.Выполнить();
ВыборкаГНК = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ГНК");

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

Резюме

Внедрение этих несложных шагов позволило нам:

← На главную