Как гарантированно выполнить обработчик один раз после создания и успешной записи нового элемента справочника в 1С?

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

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

На первый взгляд задача кажется простой, но при детальном анализе мы сталкиваемся с транзакционными рисками. Если выполнять отправку непосредственно в процедурах ПриЗаписи или ПередЗаписью, мы рискуем отправить ложное уведомление в случае последующего отката транзакции. Кроме того, сетевые задержки при отправке писем внутри транзакции могут заблокировать работу СУБД для других пользователей.

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

В чем сложность? Анализируем жизненный цикл записи объекта

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

  1. ПередЗаписью (модуль объекта): В этом событии мы можем легко определить, что объект новый, с помощью метода ЭтоНовый(). Однако транзакция базы данных только начинается. Если на следующем шаге возникнет ошибка блокировки или сработает отказ в проверке заполнения, транзакция откатится, элемент не создастся, но письмо уже улетит получателю.
  2. ПриЗаписи (модуль объекта): Здесь объект уже физически записан в базу данных, но сама транзакция записи еще не зафиксирована. Любая ошибка в последующих подписках на события или в коде платформы приведет к откату транзакции. Также отправка писем (работа по протоколу SMTP) внутри транзакции — это грубейшая архитектурная ошибка. Сетевое ожидание ответа почтового сервера может длиться до нескольких секунд, в течение которых запись в таблице базы данных будет заблокирована. Это гарантированно вызовет взаимные блокировки (deadlocks) у других пользователей.
  3. ПослеЗаписи (модуль формы): Это событие происходит на клиенте вне транзакции. Оно отлично подходит для интерактивной работы, но совершенно бесполезно, если новые элементы справочника создаются программно (например, через загрузку из Excel, веб-сервисы или планы обмена).

Выяснив причины проблем, перейдем к рассмотрению надежных вариантов реализации.

Способ 1. Использование «Дополнительных свойств» объекта (для простых интерактивных сценариев)

Если в вашей системе объекты создаются в основном интерактивно пользователями через форму, мы можем применить связку событий ПередЗаписью и ПослеЗаписиНаСервере формы. Для этого передадим признак «новизны» объекта через коллекцию ДополнительныеСвойства.

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

Шаг 1. В модуле объекта справочника перехватываем признак того, что объект является новым, и помещаем его во временную структуру:


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

Шаг 2. В модуле формы справочника используем событие ПослеЗаписиНаСервере. Это событие срабатывает сразу после успешного завершения и фиксации транзакции записи на сервере, но контекст формы еще сохраняет связь с объектом:


&НаСервере
Процедура ПослеЗаписиНаСервере(ТекущийОбъект, ПараметрыЗаписи)
    
    // Проверяем наличие нашего признака в дополнительных свойствах записанного объекта
    Если ТекущийОбъект.ДополнительныеСвойства.Свойство("ЭтоНовыйОбъект") 
        И ТекущийОбъект.ДополнительныеСвойства.ЭтоНовыйОбъект Тогда
        
        // Удаляем свойство, чтобы при повторной записи (без закрытия формы) код не сработал еще раз
        ТекущийОбъект.ДополнительныеСвойства.Удалить("ЭтоНовыйОбъект");
        
        // Вызываем процедуру отправки уведомления вне транзакции записи
        ОтправитьУведомлениеОСоздании(ТекущийОбъект.Ссылка);
        
    КонецЕсли;
    
КонецПроцедуры

Преимущества: Простота реализации, отсутствие необходимости создавать новые метаданные в конфигурации.

Ограничения: Метод не сработает при чисто программном создании элементов без открытия формы.

Способ 2. Паттерн «Фоновое задание + Управляемая блокировка» (для программной записи без создания таблиц)

Как гарантировать отправку только при успешном программном создании объектов без привлечения дополнительных регистров сведений? Разберем элегантный и продвинутый прием, который имитирует событие «После фиксации транзакции».

Идея заключается в следующем: в транзакции записи мы запускаем асинхронное фоновое задание. В самом фоновом задании мы пытаемся наложить управляемую блокировку на создаваемый элемент. Поскольку транзакция создания еще активна, фоновое задание «подвиснет» на строке блокировки и будет ожидать. Как только транзакция завершится (успешно или откатом), блокировка снимется, и фоновое задание мгновенно продолжит работу.

Посмотрим на пример реализации. В модуле объекта пишем:


Процедура ПриЗаписи(Отказ)
    
    Если Отказ Или ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Если флаг нового объекта был взведен в "ПередЗаписью"
    Если ДополнительныеСвойства.Свойство("ЭтоНовыйОбъект") Тогда
        
        ПараметрыЗапуска = Новый Массив;
        ПараметрыЗапуска.Добавить(Ссылка);
        
        // Запускаем асинхронную процедуру
        ФоновыеЗадания.Выполнить("ОбщегоНазначенияСервер.ВыполнитьПослеЗаписиЭлемента", ПараметрыЗапуска);
        
    КонецЕсли;
    
КонецПроцедуры

Теперь создадим процедуру в общем модуле ОбщегоНазначенияСервер (свойства модуля: Сервер, Вызов сервера):


Процедура ВыполнитьПослеЗаписиЭлемента(СсылкаНаЭлемент) Экспорт
    
    НачатьТранзакцию();
    Попытка
        // Накладываем управляемую блокировку на элемент
        Блокировка = Новый БлокировкаДанных;
        ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура"); // Укажите ваш справочник
        ЭлементБлокировки.УстановитьЗначение("Ссылка", СсылкаНаЭлемент);
        ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
        
        // Поток фонового задания остановится здесь до завершения основной транзакции записи
        Блокировка.Заблокировать();
        
        // Проверяем, существует ли элемент в БД. Если транзакция откатилась - объект не будет найден
        ЭлементОбъекта = СсылкаНаЭлемент.ПолучитьОбъект();
        Если ЭлементОбъекта = Неопределено Тогда
            ОтменитьТранзакцию();
            Возврат; // Объект не записан, тихо выходим
        КонецЕсли;
        
        // Объект гарантированно зафиксирован в базе. Выполняем полезное действие!
        ОтправитьПисьмоПолучателю(СсылкаНаЭлемент);
        
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ЗаписатьЛогОшибки(ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

Преимущества: Работает при любом способе создания элемента (интерактивном или программном), не требует добавления новых реквизитов или регистров.

Риски: При массовом создании элементов (например, групповой импорт тысяч позиций) моментальный запуск сотен фоновых заданий создаст критическую нагрузку на сервер 1С («Server Thrashing»). Этот метод подходит только для единичного создания документов или элементов.

Способ 3. Гарантированная отправка через Очередь задач (Регистр сведений)

Если надежность доставки писем критически важна, а новые элементы могут создаваться массово, лучшим решением будет использование паттерна «Очередь задач». Мы разделяем процесс регистрации события и сам процесс отправки уведомлений.

Выясним схему работы этого паттерна:

  1. При записи нового элемента мы лишь фиксируем намерение отправить письмо, делая быструю запись в специальный вспомогательный регистр сведений (например, ОчередьОтправкиПисем). Это происходит в той же транзакции и выполняется мгновенно.
  2. Отдельное регламентное задание с периодичностью раз в несколько минут обрабатывает этот регистр, отправляет скопившиеся письма и очищает очередь.

Рассмотрим код записи в очередь в модуле объекта справочника:


Процедура ПриЗаписи(Отказ)
    
    Если Отказ Или ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    Если ДополнительныеСвойства.Свойство("ЭтоНовыйОбъект") Тогда
        
        // Создаем запись в регистре очереди
        НаборЗаписей = РегистрыСведений.ОчередьОтправкиПисем.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.ОбъектОтправки.Установить(Ссылка);
        
        НоваяСтрока = НаборЗаписей.Добавить();
        НоваяСтрока.ОбъектОтправки = Ссылка;
        НоваяСтрока.ДатаРегистрации = ТекущаяДатаСеанса();
        НоваяСтрока.Состояние = Перечисления.СостоянияОтправки.КОтправке;
        
        НаборЗаписей.Записать(Истина); // Запись происходит в текущей транзакции
        
    КонецЕсли;
    
КонецПроцедуры

Обработчик регламентного задания будет выглядеть следующим образом:


Процедура ВыполнитьОтправкуПисемИзОчереди() Экспорт
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Очередь.ОбъектОтправки КАК ОбъектОтправки
        |ИЗ
        |	РегистрСведений.ОчередьОтправкиПисем КАК Очередь
        |ГДЕ
        |	Очередь.Состояние = ЗНАЧЕНИЕ(Перечисление.СостоянияОтправки.КОтправке)";
        
    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        // Проверяем "битые" ссылки (на случай, если транзакция создания каким-то чудом отменилась)
        Если Выборка.ОбъектОтправки.ПолучитьОбъект() = Неопределено Тогда
            ИсключитьИзОчереди(Выборка.ОбъектОтправки);
            Продолжить;
        КонецЕсли;
        
        Попытка
            ОтправитьПочтовоеСообщение(Выборка.ОбъектОтправки);
            ИсключитьИзОчереди(Выборка.ОбъектОтправки);
        Исключение
            // При ошибке отправки статус можно изменить на "Ошибка" для последующего анализа
            ЗаписатьОшибкуОчереди(Выборка.ОбъектОтправки, ОписаниеОшибки());
        КонецПопытки;
        
    КонецЦикла;
    
КонецПроцедуры

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

Способ 4. Использование штатных механизмов БСП (Библиотека стандартных подсистем)

Если вы работаете в типовой конфигурации (например, 1С:ERP, УТ, КА, УНФ или БП), то изобретать велосипед с собственными регистрами не требуется. В составе БСП уже есть готовые инструменты для решения этих задач:

Подведем итоги

Выбор конкретного решения зависит от требований вашего бизнес-процесса:

  1. Если отправка писем нужна только при ручной работе пользователей, используйте Способ 1 (ДополнительныеСвойства) в связке с клиентским событием формы.
  2. Если требуется быстрая автоматизация без изменения структуры метаданных для редких программных записей, применяйте Способ 2 (Фоновое задание с управляемой блокировкой).
  3. Для высоконагруженных и критически важных систем единственным верным решением будет разделение транзакций через очередь задач — Способ 3 (Регистры очереди) или штатные механизмы БСП (Способ 4).
← На главную