В процессе разработки в среде 1С:Предприятие 8 программисты часто сталкиваются с задачей, когда при записи элемента справочника или документа необходимо выполнить сопутствующее действие — например, отправить электронное письмо контрагенту или менеджеру — и в случае успешной отправки зафиксировать этот факт, установив флаг (булевый реквизит) прямо в записываемом объекте.
На первый взгляд задача кажется тривиальной, однако при её реализации на управляемых формах разработчики часто совершают архитектурные ошибки, которые приводят либо к потере данных, либо к критическому зависанию всей информационной базы. В этой статье мы подробно разберем, почему стандартные «интуитивные» подходы не работают, изучим правильный механизм взаимодействия формы и модуля объекта, а также рассмотрим профессиональные архитектурные решения для безопасной отправки почты.
Рассмотрим типичный сценарий ошибки, с которой часто начинают начинающие разработчики. В модуле управляемой формы в процедуре ПриЗаписиНаСервере пишется код вида:
&НаСервере
Процедура ПриЗаписиНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Попытка установить флаг
ТекущийОбъект.ФлагПисьмоОтправлено = Истина;
КонецПроцедуры
При возврате управления на клиент в реквизите Объект на форме флаг действительно принимает значение Истина, однако если мы откроем базу данных напрямую или обратимся к Объект.Ссылка.ФлагПисьмоОтправлено, то обнаружим там значение Ложь. Почему так происходит?
Выясним причину, проанализировав последовательность событий при записи объекта из формы базы данных:
ПередЗаписьюНаСервере.ПередЗаписью в модуле самого объекта.ПриЗаписи в модуле объекта.ПриЗаписиНаСервере в модуле формы. В этот момент запись в базу данных уже совершена! СУБД зафиксировала те значения реквизитов, которые были у объекта на шаге 4.Когда мы изменяем реквизиты у переменной ТекущийОбъект внутри процедуры ПриЗаписиНаСервере, мы меняем состояние объекта, находящегося исключительно в оперативной памяти сервера. Эти изменения возвращаются на форму в клиентский сеанс, но повторной записи в базу данных автоматически не происходит. Пытаться вызвать метод ТекущийОбъект.Записать() внутри этого же обработчика категорически запрещено — это приведет к бесконечной рекурсии и аварийному завершению работы системы.
Чтобы изменения реквизитов гарантированно сохранились в базе данных в рамках той же транзакции, их необходимо выполнять до того, как СУБД запишет данные на диск. Лучшее место для этого — обработчик ПередЗаписью в модуле объекта.
Однако здесь мы сталкиваемся со следующей проблемой: отправка письма инициируется пользователем интерактивно с формы, а модуль объекта является универсальным и ничего не знает о контексте формы. Запись элемента справочника может происходить программно (например, при обмене данными, групповой обработке или загрузке из внешних систем), когда отправлять реальные письма пользователям категорически нельзя.
Для передачи управляющих параметров из формы в модуль объекта используется специальная коллекция ДополнительныеСвойства.
Рассмотрим по шагам, как настроить безопасную передачу флага отправки письма из формы в модуль объекта.
Шаг 1. Модуль формы справочника
В модуле формы в обработчике ПередЗаписьюНаСервере мы проверяем, нужно ли инициировать отправку, и помещаем специальный признак в коллекцию дополнительных свойств объекта.
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Проверяем модифицированность формы, чтобы не слать письма при холостой записи
Если ЭтаФорма.Модифицированность Тогда
// Записываем наш параметр-сигнал в структуру ДополнительныеСвойства
ТекущийОбъект.ДополнительныеСвойства.Вставить("НужноОтправитьПисьмо", Истина);
КонецЕсли;
КонецПроцедуры
Шаг 2. Модуль объекта справочника
В модуле объекта в обработчике ПередЗаписью мы перехватываем этот параметр, выполняем логику отправки и взводим флаг.
Процедура ПередЗаписью(Отказ)
// Правило хорошего тона №1: Всегда проверяем обмен данными
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Извлекаем переданный параметр из формы
НужноОтправить = Ложь;
Если ДополнительныеСвойства.Свойство("НужноОтправитьПисьмо", НужноОтправить) И НужноОтправить Тогда
// Проверяем, не было ли письмо отправлено ранее
Если Не ЭтотОбъект.ФлагПисьмоОтправлено Тогда
// Выполняем отправку письма
УспешнаяОтправка = ОтправитьПисьмоКонтрагенту();
Если УспешнаяОтправка Тогда
// Устанавливаем флаг в самом объекте. Он запишется в БД автоматически!
ЭтотОбъект.ФлагПисьмоОтправлено = Истина;
Иначе
// Если отправка критична, мы можем отменить транзакцию записи объекта
Отказ = Истина;
ВызватьИсключение "Не удалось отправить уведомление. Сохранение объекта отменено.";
EndIf;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Описанный выше метод полностью работоспособен, однако он таит в себе серьезную опасность для многопользовательских систем. Отправка электронного письма выполняется по протоколу SMTP, что является внешней сетевой операцией.
Проанализируем риски выполнения сетевых запросов внутри транзакции СУБД:
Золотое правило разработки в 1С: Внутри транзакции (между началом записи и её завершением) не должно быть никаких длительных операций, сетевых запросов к внешним API, отправки почты или сложных расчетов.
Чтобы избежать транзакционных блокировок, мы должны разделить процесс записи объекта и процесс отправки почты. Вместо мгновенной отправки письма внутри транзакции мы будем регистрировать задание на отправку.
Разберем по шагам реализацию асинхронной схемы через специальный регистр сведений:
ОчередьОтправкиПисем с измерениями Объект (тип Ссылка на наш справочник) и ресурсом ДатаРегистрации.ОчередьОтправкиПисем, отправляет реальные письма во внесистемном потоке, а после успешной отправки устанавливает флаг ФлагПисьмоОтправлено = Истина у объекта и удаляет запись из очереди.Посмотрим на пример реализации кода в модуле объекта для асинхронного подхода:
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
НужноОтправить = Ложь;
Если ДополнительныеСвойства.Свойство("НужноОтправитьПисьмо", НужноОтправить) И НужноОтправить Тогда
Если Не ЭтотОбъект.ФлагПисьмоОтправлено Тогда
// Вместо отправки пишем задание в регистр сведений
НаборЗаписей = РегистрыСведений.ОчередьОтправкиПисем.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Объект.Установить(ЭтотОбъект.Ссылка);
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Объект = ЭтотОбъект.Ссылка;
НоваяЗапись.ДатаРегистрации = ТекущаяДата();
// Набор записей запишется в базу данных в этой же транзакции
НаборЗаписей.Записать();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Регламентное задание, работающее в фоне, выполнит отправку почты. После этого оно обновит реквизит справочника, используя следующий код для отключения повторного срабатывания бизнес-логики:
// Код внутри обработчика регламентного задания
ЭлементОбъект = СсылкаНаОбъект.ПолучитьОбъект();
ЭлементОбъект.ФлагПисьмоОтправлено = Истина;
// Отключаем проверки и обработчики, чтобы не спровоцировать рекурсию или повторную очередь
ЭлементОбъект.ОбменДанными.Загрузка = Истина;
ЭлементОбъект.Записать();
Подводя итоги, сформулируем основные правила реализации связанных действий при записи объектов в 1С:
ПриЗаписиНаСервере с целью их сохранения — в этот момент транзакция записи СУБД уже прошла физический этап фиксации данных.ДополнительныеСвойства.ОбменДанными.Загрузка, чтобы избежать дублирования писем при миграции данных в РИБ или обмене.