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