В крупных корпоративных системах на базе 1С:Управление производственным предприятием (УПП 1.3) часто используются отраслевые или кастомные интеграционные механизмы. Одним из таких популярных решений является интеграция с шиной данных RabbitMQ (например, от компании «Первый БИТ»). При разработке регламентных заданий, которые массово изменяют реквизиты объектов, программисты часто сталкиваются с неочевидными проблемами: падением производительности базы данных, блокировками таблиц и сбоями при регистрации измененных данных в очереди обмена.
В этой статье мы подробно разберем реальный кейс оптимизации регламентного задания, которое должно снимать признак СТ_R_AndD (исследования и разработки) у групп номенклатуры и автоматически регистрировать связанную номенклатуру к обмену в RabbitMQ. Мы пошагово исправим критические архитектурные ошибки исходного кода, ускорим работу алгоритма в десятки раз и обеспечим надежную транзакционную модель.
Перед разработчиком стояла задача: два раза в неделю запускать регламентное задание, которое находит группы номенклатуры в каталоге (ГНК) с истекшим сроком действия статуса "RnD", снимает этот флаг, а затем регистрирует всю подчиненную номенклатуру (НК) к обмену, чтобы изменения улетели во внешние системы через брокер сообщений RabbitMQ.
В исходном варианте кода автор допустил несколько серьезных ошибок:
Поле объекта не обнаружено (ДополнительныеСвойства).Давайте подробно разберем каждую ошибку и найдем оптимальные решения.
---
Давайте обратим внимание на фрагмент исходного кода, который вызвал бурную реакцию экспертов на форуме:
Пока ВыборкаГНК.Следующий цикл
СправочникОбъект = ВыборкаГНК.ГНК.ПолучитьОбъект;
СправочникОбъект.СТ_R_AndD = Ложь;
// ...
// ОШИБКА: Повторное выполнение всего запроса на каждой итерации!
ВыборкаНоменклатура = Запрос.Выполнить.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "НК");
Пока ВыборкаНоменклатура.Следующий Цикл
НоменклатураОбъект = ВыборкаНоменклатура.Номенклатура.ПолучитьОбъект;
НоменклатураОбъект.Записать;
КонецЦикла;
КонецЦикла;
При вызове метода Запрос.Выполнить() внутри цикла система обращается к СУБД (MS SQL Server / PostgreSQL) заново на каждом шаге. Если регламентное задание обрабатывает 500 групп номенклатуры, запрос выполнится 501 раз! Это приводит к колоссальной нагрузке на процессор сервера и дисковую подсистему.
Для исправления этой архитектурной ошибки мы должны выполнить запрос ровно один раз. Чтобы обойти иерархическую структуру «Группа номенклатуры -> Номенклатура», воспользуемся встроенными возможностями обхода результатов запроса по группировкам. Метод Выбрать() нужно вызывать не у самого запроса, а у текущей строки родительской выборки:
РезультатЗапроса = Запрос.Выполнить();
// Выбираем группировку верхнего уровня (ГНК)
ВыборкаГНК = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ГНК");
Пока ВыборкаГНК.Следующий() Цикл
// Обрабатываем ГНК...
// ПРАВИЛЬНО: получаем подчиненную выборку (номенклатуру) только для ТЕКУЩЕЙ ГНК
ВыборкаНоменклатура = ВыборкаГНК.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Номенклатура");
Пока ВыборкаНоменклатура.Следующий() Цикл
// Обрабатываем номенклатуру без повторного обращения к БД за запросом!
КонецЦикла;
КонецЦикла;
---
При попытке передать номенклатуру в специализированный модуль интеграции БИТ вылетала ошибка:
{ОбщийМодуль.СТ_ВыгрузкаRabbitMQ.Модуль(20)}: Поле объекта не обнаружено (ДополнительныеСвойства)
Проанализируем причину. Код модуля интеграции проверяет структуру ДополнительныеСвойства у источника:
Процедура адаптер_ПриЗаписиЭлементаПриЗаписи(Источник, Отказ) Экспорт
Если Не Источник.ДополнительныеСвойства.Свойство("НеПередаватьОбъектПоRabbitMQ") Тогда
...
В платформе 1С тип данных СправочникСсылка представляет собой лишь ссылку на запись в базе данных и не содержит коллекции ДополнительныеСвойства. Эта коллекция доступна только у типа СправочникОбъект, который считывается в оперативную память для модификации.
Следовательно, передавать в интеграционный модуль нужно исключительно объект, предварительно полученный с помощью метода ПолучитьОбъект().
// ОШИБКА: Передача ссылки приведет к падению системы
СТ_ВыгрузкаRabbitMQ.адаптер_ПриЗаписиЭлементаПриЗаписи(ВыборкаНоменклатура.Номенклатура, Отказ);
// ПРАВИЛЬНО: Получаем объект и передаем его в процедуру регистрации RMQ
НоменклатураОбъект = ВыборкаНоменклатура.Номенклатура.ПолучитьОбъект();
СТ_ВыгрузкаRabbitMQ.адаптер_ПриЗаписиЭлементаПриЗаписи(НоменклатураОбъект, Ложь);
---
В исходном коде для отправки изменений в шину выполнялась непосредственная запись каждого элемента номенклатуры в базу данных:
НоменклатураОбъект.Записать();
Давайте подумаем: сама номенклатура физически никак не изменилась (мы сняли флаг только у группы номенклатуры ГНК). Запись неизмененного объекта в базу данных — это невероятно затратная операция. Платформа 1С при вызове метода Записать() производит:
ПередЗаписью и ПриЗаписи (а в УПП их может быть огромное количество).Поскольку логика RabbitMQ в решениях БИТ формирует XML-пакет напрямую из структуры переданного объекта, нам совершенно не обязательно записывать номенклатуру в СУБД. Достаточно получить объект в память, инициализировать его свойства и вызвать процедуру отправки адаптера. Это позволит избежать лишней нагрузки на диски и процессор сервера.
// Оптимальный вариант без перезаписи элемента в БД
НоменклатураОбъект = ВыборкаНоменклатура.Номенклатура.ПолучитьОбъект();
Если НоменклатураОбъект <> Неопределено Тогда
// Просто регистрируем изменения в шине без выполнения НоменклатураОбъект.Записать()!
СТ_ВыгрузкаRabbitMQ.адаптер_ПриЗаписиЭлементаПриЗаписи(НоменклатураОбъект, Ложь);
КонецЕсли;
---
Использование конструкций НачатьТранзакцию() и Попытка...Исключение в кодовой базе автора содержало опасный паттерн "молчаливого падения" (silent failure):
Исключение
Успех = Ложь;
КонецПопытки;
Если Успех Тогда
ЗафиксироватьТранзакцию;
Иначе
ОтменитьТранзакцию;
КонецЕсли;
Если при записи ГНК или отправке сообщения произойдет системный сбой (например, блокировка базы данных или сетевой таймаут шины), транзакция откатится, но администраторы и разработчики об этом даже не узнают! В журнале регистрации не останется никаких следов. К тому же, в случае возникновения ошибки СУБД внутри транзакции, она переходит в состояние "битой" (doomed transaction), и любые последующие манипуляции до явного вызова отмены могут приводить к непредсказуемым ошибкам платформы.
Давайте реализуем надежный и безопасный шаблон обработки транзакций с обязательным логированием критических ошибок:
НачатьТранзакцию();
Попытка
СправочникОбъект.Записать();
// ... логика отправки уведомлений ...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Записываем ошибку в Журнал регистрации для последующего анализа администратором
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
ЗаписьЖурналаРегистрации(
"РегламентноеЗадание.СнятиеRnD",
УровеньЖурналаРегистрации.Ошибка,
ВыборкаГНК.ГНК.Метаданные(),
ВыборкаГНК.ГНК,
"Не удалось обработать группу номенклатуры: " + ВыборкаГНК.ГНК + Символы.ПС + ТекстОшибки
);
КонецПопытки;
---
Еще одна классическая ошибка оптимизации — вызов методов получения констант внутри тела цикла. Даже если используется модуль с повторным использованием значений (БИТ_СТ_ПовторноеИспользованиеЗначений), само обращение к контексту модуля требует времени.
Давайте получим шаблон сообщения и адреса рассылки один раз в самом начале нашей процедуры:
// Получаем константы ОДИН раз перед выполнением циклов
ШаблонСообщения = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("СТ_ШаблонСообщенияСнятиеRndГНК");
АдресаПолучателей = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("АдресаПолучателейДляОповещенияПоИзменениюПараметра_R_AndD");
---
Давайте соберем все наши архитектурные улучшения в единый красивый, быстрый и отказоустойчивый алгоритм:
УстановитьПривилегированныйРежим(Истина);
// 1. Предварительно получаем константы
ШаблонСообщения = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("СТ_ШаблонСообщенияСнятиеRndГНК");
АдресаПолучателей = БИТ_СТ_ПовторноеИспользованиеЗначений.ПолучитьЗначениеКонстанты("АдресаПолучателейДляОповещенияПоИзменениюПараметра_R_AndD");
// 2. Инициализируем и выполняем запрос
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДатаПроверки", ТекущаяДата());
Запрос.Текст =
"ВЫБРАТЬ
| БИТ_СТ_ГруппыНоменклатурыВКаталоге.Ссылка КАК ГНК,
| Номенклатура.БИТ_СТ_ГруппаНоменклатуры.ОтветственныйКонструктор КАК ОтветственныйКонструктор,
| Номенклатура.Ссылка КАК Номенклатура
|ИЗ
| Справочник.БИТ_СТ_ГруппыНоменклатурыВКаталоге КАК БИТ_СТ_ГруппыНоменклатурыВКаталоге
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Номенклатура
| ПО (Номенклатура.БИТ_СТ_ГруппаНомеклатурыВКаталоге = БИТ_СТ_ГруппыНоменклатурыВКаталоге.Ссылка)
| И (Номенклатура.БИТ_СТ_ГруппаНоменклатуры.ОтветственныйКонструктор <> ЗНАЧЕНИЕ(Справочник.Пользователи.ПустаяСсылка))
|ГДЕ
| НЕ БИТ_СТ_ГруппыНоменклатурыВКаталоге.ПометкаУдаления
| И БИТ_СТ_ГруппыНоменклатурыВКаталоге.СТ_R_AndD
| И БИТ_СТ_ГруппыНоменклатурыВКаталоге.СТ_ПлановаяДатаСнятия <= &ДатаПроверки
|ИТОГИ ПО
| ГНК,
| Номенклатура";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаГНК = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ГНК");
Пока ВыборкаГНК.Следующий() Цикл
СправочникОбъект = ВыборкаГНК.ГНК.ПолучитьОбъект();
Если СправочникОбъект = Неопределено Тогда
Продолжить;
КонецЕсли;
СправочникОбъект.СТ_R_AndD = Ложь;
СправочникОбъект.СТ_ДатаУстановкиСтатуса = ТекущаяДата();
// Начинаем транзакцию для изменения текущей ГНК
НачатьТранзакцию();
Попытка
СправочникОбъект.Записать();
// Регистрируем изменения подчиненной номенклатуры в RabbitMQ
ВыборкаНоменклатура = ВыборкаГНК.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Номенклатура");
Пока ВыборкаНоменклатура.Следующий() Цикл
Если ЗначениеЗаполнено(ВыборкаНоменклатура.Номенклатура) Тогда
НоменклатураОбъект = ВыборкаНоменклатура.Номенклатура.ПолучитьОбъект();
Если НоменклатураОбъект <> Неопределено Тогда
// Регистрируем изменения в шине без физической записи в базу данных!
СТ_ВыгрузкаRabbitMQ.адаптер_ПриЗаписиЭлементаПриЗаписи(НоменклатураОбъект, Ложь);
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Отправка системных оповещений по шаблону
СтруктураКонтекст = СформироватьСтруктуруКонтекст(ВыборкаГНК.ГНК);
СтруктураСообщения = бит_фн_ОповещенияСервер.СформироватьСообщениеПоШаблону(ШаблонСообщения, СтруктураКонтекст);
Если ЗначениеЗаполнено(СправочникОбъект.СТ_ОтветственныйКонструктор) Тогда
БИТ_СТ_Оповещения.ЗаписатьОповещение(
ВыборкаГНК.ГНК,
СправочникОбъект.СТ_ОтветственныйКонструктор,
СтруктураСообщения.Заголовок,
СтруктураСообщения.Текст,
СтруктураСообщения.Подпись,
Неопределено
);
КонецЕсли;
Если ЗначениеЗаполнено(АдресаПолучателей) Тогда
БИТ_СТ_Оповещения.ЗаписатьОповещение(
ВыборкаГНК.ГНК,
Неопределено,
СтруктураСообщения.Заголовок,
СтруктураСообщения.Текст,
СтруктураСообщения.Подпись,
АдресаПолучателей
);
КонецЕсли;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
ЗаписьЖурналаРегистрации(
"РегламентноеЗадание.СнятиеRnD",
УровеньЖурналаРегистрации.Ошибка,
ВыборкаГНК.ГНК.Метаданные(),
ВыборкаГНК.ГНК,
"Ошибка обработки ГНК " + ВыборкаГНК.ГНК + ": " + ТекстОшибки
);
КонецПопытки;
// Дополнительное оповещение ответственных конструкторов
ВыборкаКонструктор = ВыборкаГНК.Выбрать();
Пока ВыборкаКонструктор.Следующий() Цикл
Если Не ЗначениеЗаполнено(ВыборкаКонструктор.ОтветственныйКонструктор) Тогда
Продолжить;
КонецЕсли;
НачатьТранзакцию();
Попытка
БИТ_СТ_Оповещения.ЗаписатьОповещение(
ВыборкаГНК.ГНК,
ВыборкаКонструктор.ОтветственныйКонструктор,
СтруктураСообщения.Заголовок,
СтруктураСообщения.Текст,
СтруктураСообщения.Подпись,
Неопределено
);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
ЗаписьЖурналаРегистрации(
"РегламентноеЗадание.СнятиеRnD",
УровеньЖурналаРегистрации.Предупреждение,
ВыборкаГНК.ГНК.Метаданные(),
ВыборкаГНК.ГНК,
"Ошибка отправки оповещения конструктору " + ВыборкаКонструктор.ОтветственныйКонструктор + ": " + ТекстОшибки
);
КонецПопытки;
КонецЦикла;
КонецЦикла;
Внедрение этих несложных шагов позволило нам:
ДополнительныеСвойства за счет правильной работы с объектом справочника.