При разработке функционала в 1С часто возникает необходимость выполнять определенные действия после успешного проведения документа, например, отправлять уведомления пользователям. Однако, механизм транзакций в 1С имеет свои особенности, которые могут привести к нежелательным результатам, если эти особенности не учитывать. Рассмотрим типичную ситуацию, с которой сталкиваются разработчики, и проанализируем, как правильно ее разрешить.
Предположим, у нас есть задача отправлять сообщение в Telegram ответственному пользователю каждый раз, когда проводится новый документ. Очевидным решением кажется создание подписки на событие ОбработкаПроведения для этого документа. В этой подписке мы можем выполнить все необходимые проверки и, если документ успешно проводится, отправить сообщение.
Однако, здесь возникает важный нюанс. Документ может проводиться как интерактивно (через форму пользователем), так и неинтерактивно (программно, например, через обмен данными или регламентное задание). В случае интерактивного проведения, после того как подписка на ОбработкаПроведения уже отработала, может сработать событие ПриЗаписиНаСервере в модуле формы. В этом событии, или в любом другом обработчике в рамках транзакции записи, можно установить параметр Отказ в Истина. Если это произойдет, вся транзакция записи документа будет отменена.
Именно здесь кроется проблема: наша подписка на ОбработкаПроведения уже выполнила свои действия, включая потенциальную отправку сообщения в Telegram, но документ в итоге не был проведен. Транзакция была отменена, а мы об этом не узнали. Как же нам гарантировать, что сообщение будет отправлено только в том случае, если документ действительно успешно проведен и транзакция зафиксирована?
Прежде чем перейти к решениям, давайте кратко рассмотрим, как устроены транзакции в 1С. Понимание этих принципов критически важно для корректной работы с данными.
ПриЗаписиНаСервере или ОбработкаПроведения, установить параметр Отказ в Истина, то вся текущая транзакция будет отменена. Все изменения, которые были сделаны в рамках этой транзакции, включая те, что были выполнены в подписках на события, будут аннулированы. Именно это и является причиной нашей проблемы.НачатьТранзакцию(), ЗафиксироватьТранзакцию() и ОтменитьТранзакцию() внутри уже открытой транзакции (например, транзакции записи документа), то фактически вы работаете с транзакцией верхнего уровня. Отмена изменений во "вложенной" транзакции приведет к отмене всех изменений транзакции верхнего уровня.ОтменитьТранзакцию(), что обычно делается в блоке Исключение конструкции Попытка-Исключение.Исходя из этих принципов, становится очевидно, что любое действие, которое должно быть выполнено строго после успешной фиксации транзакции (т.е. после того, как мы убедились, что документ действительно записан и проведен), должно быть вынесено за пределы основной транзакции записи. Рассмотрим два основных подхода к решению этой задачи.
Наиболее распространенный и гибкий способ гарантировать выполнение действий только после успешного завершения транзакции – это использование фоновых заданий. Этот подход позволяет отвязать длительные или необратимые операции (такие как отправка сообщений) от основной транзакции записи документа.
ОбработкаПроведения (или любом другом месте, которое выполняется в рамках транзакции, но до потенциального Отказ = Истина) мы создаем и планируем фоновое задание.Рассмотрим, как это можно реализовать шаг за шагом:
// Общий модуль, например, "МодульУведомленийСервер"
Процедура ОтправитьУведомлениеОПроведенииДокументаФоново(ИДДокумента, ВерсияДанныхДокумента) Экспорт
Попытка
// 1. Получаем ссылку на документ
ДокументСсылка = Документы.НашДокумент.ПолучитьСсылку(ИДДокумента);
// 2. Проверяем существование документа и его актуальность
Если ДокументСсылка.Пустая() Тогда
// Документ не найден, возможно, был удален или транзакция откатилась
Возврат;
КонецЕсли;
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
Если ДокументОбъект = Неопределено Тогда
// Объект не получен, возможно, документ был удален после получения ссылки,
// или к нему нет доступа (редкий случай для фонового задания)
Возврат;
КонецЕсли;
Если ДокументОбъект.ВерсияДанных <> ВерсияДанныхДокумента Тогда
// Версия данных не совпадает. Это означает, что документ был изменен или
// исходная транзакция была отменена и создан новый документ с тем же ИД
// (что маловероятно) или просто документ был изменен.
// В любом случае, это не та версия, о которой мы хотели уведомить.
Возврат;
КонецЕсли;
// 3. Документ существует и его версия совпадает.
// Значит, транзакция успешно завершена, и документ проведен.
// Теперь можно отправлять уведомление.
Сообщить("Документ " + ДокументОбъект + " успешно проведен. Отправляем уведомление в Telegram.");
// Здесь размещаем код для отправки сообщения в Telegram
// Например:
// ТелеграмБот.ОтправитьСообщение(ОтветственныйПользователь.ИдентификаторTelegram,
// "Документ " + ДокументОбъект + " проведен!");
Исключение
// Обработка ошибок, возникших в фоновом задании.
// Можно записать в журнал регистрации, отправить администратору.
ЗаписьЖурналаРегистрации(
"ОшибкаОтправкиУведомления",
УровеньЖурналаРегистрации.Ошибка,
Документы.НашДокумент,
ДокументСсылка,
"Ошибка при отправке уведомления для документа " + ДокументСсылка + ": " + ОписаниеОшибки()
);
КонецПопытки;
КонецПроцедуры
ФоновыеЗадания.Создать().
// Подписка на событие "ОбработкаПроведения" для документа "НашДокумент"
Процедура ПодпискаНаОбработкуПроведенияДокумента(Источник, Отказ, РежимПроведения) Экспорт
Если Отказ Тогда
// Если Отказ уже установлен, то дальнейшие действия бессмысленны
Возврат;
КонецЕсли;
// Здесь могут быть ваши основные проверки и логика проведения
// ...
// Создаем фоновое задание для отправки уведомления
Попытка
ФоновыеЗадания.Создать(
"МодульУвеведомленийСервер.ОтправитьУведомлениеОПроведенииДокументаФоново",
Источник.УникальныйИдентификатор(),
Источник.ВерсияДанных,
"Отправка уведомления о проведении документа " + Источник
);
Исключение
// Если по каким-то причинам не удалось создать фоновое задание,
// можно записать это в журнал регистрации или принять другие меры.
ЗаписьЖурналаРегистрации(
"ОшибкаСозданияФоновогоЗадания",
УровеньЖурналаРегистрации.Предупреждение,
Источник.Метаданные(),
Источник,
"Не удалось создать фоновое задание для отправки уведомления о проведении документа " + Источник + ": " + ОписаниеОшибки()
);
КонецПопытки;
КонецПроцедуры
Важные моменты при работе с фоновыми заданиями:
УникальныйИдентификатор() и ВерсияДанных является правильной практикой.Для систем с большим количеством документов, которые могут проводиться параллельно, или для более сложных сценариев, где требуется гарантированная доставка уведомлений и возможность их повторной обработки в случае сбоев, можно использовать механизм планов обмена с авторегистрацией. Этот подход представляет собой более "тяжелое", но и более надежное решение, фактически реализуя очередь сообщений.
УведомленияОПроведении), который будет использоваться только для регистрации документов, требующих уведомления.Рассмотрим, как это можно реализовать:
УведомленияОПроведенииДокументов.
Документ.НашДокумент).
// Пример создания узла для плана обмена (можно выполнить один раз при старте системы)
УзелУведомлений = ПланыОбмена.УведомленияОПроведенииДокументов.СоздатьЭлемент();
УзелУведомлений.Наименование = "УведомленияДляРассылки";
УзелУведомлений.Код = "00001"; // Уникальный код узла
УзелУведомлений.Записать();
// Общий модуль, например, "МодульРегламентныхЗаданий"
Процедура ОбработатьУведомленияОПроведении() Экспорт
// Получаем узел, для которого регистрируются уведомления
УзелУведомлений = ПланыОбмена.УведомленияОПроведенииДокументов.НайтиПоКоду("00001"); // Используем код созданного узла
Если УзелУведомлений = Неопределено Тогда
ЗаписьЖурналаРегистрации(
"ОшибкаПланаОбмена",
УровеньЖурналаРегистрации.Ошибка,
,
,
"Не найден узел 'УведомленияДляРассылки' для плана обмена 'УведомленияОПроведенииДокументов'."
);
Возврат;
КонецЕсли;
// Начинаем выборку зарегистрированных объектов
ВыборкаОбъектов = ПланыОбмена.ПолучитьИзменения(УзелУведомлений);
Пока ВыборкаОбъектов.Следующий() Цикл
ОбъектСсылка = ВыборкаОбъектов.Ссылка;
Если ТипЗнч(ОбъектСсылка) = Тип("ДокументСсылка.НашДокумент") Тогда
// Это наш документ, который был успешно проведен
Попытка
ДокументОбъект = ОбъектСсылка.ПолучитьОбъект();
Если ДокументОбъект <> Неопределено И ДокументОбъект.Проведен Тогда
Сообщить("Документ " + ДокументОбъект + " успешно проведен. Отправляем уведомление в Telegram (через план обмена).");
// Здесь размещаем код для отправки сообщения в Telegram
// Например:
// ТелеграмБот.ОтправитьСообщение(ОтветственныйПользователь.ИдентификаторTelegram,
// "Документ " + ДокументОбъект + " проведен!");
Иначе
// Документ не найден или не проведен (возможно, был отменен после регистрации,
// или удален). В этом случае уведомлять не нужно.
ЗаписьЖурналаРегистрации(
"ДокументНеАктуален",
УровеньЖурналаРегистрации.Информация,
ОбъектСсылка.Метаданные(),
ОбъектСсылка,
"Документ " + ОбъектСсылка + " не актуален или не проведен для отправки уведомления."
);
КонецЕсли;
// Снимаем с регистрации после успешной обработки или принятия решения не уведомлять
ПланыОбмена.УдалитьРегистрациюИзменений(УзелУведомлений, ОбъектСсылка);
Исключение
// Если произошла ошибка при обработке конкретного документа,
// записываем в журнал, но не снимаем с регистрации,
// чтобы попытаться обработать его позже.
ЗаписьЖурналаРегистрации(
"ОшибкаОбработкиУведомления",
УровеньЖурналаРегистрации.Ошибка,
ОбъектСсылка.Метаданные(),
ОбъектСсылка,
"Ошибка при обработке уведомления для документа " + ОбъектСсылка + ": " + ОписаниеОшибки()
);
КонецПопытки;
Иначе
// Если в плане обмена зарегистрировались другие объекты, которые мы не хотим обрабатывать
// или для которых логика другая, мы можем их проигнорировать или обработать по-своему.
// Важно: если мы их не удалим из регистрации, они будут попадать в выборку снова.
// Например, можно удалить из регистрации без обработки:
ПланыОбмена.УдалитьРегистрациюИзменений(УзелУведомлений, ОбъектСсылка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
ОбработатьУведомленияОПроведении из МодульРегламентныхЗаданий. Настраиваем расписание выполнения, например, каждые 5-10 минут.Преимущества использования планов обмена для уведомлений:
При реализации любых отложенных действий, особенно связанных с транзакциями, важно учитывать несколько общих моментов:
ВерсияДанных, который мы используем в фоновом задании, является фундаментальным для проверки актуальности объекта. Он позволяет нам удостовериться, что документ не был изменен или откачен после того, как мы инициировали отложенное действие. Всегда используйте его для проверки перед выполнением критически важных операций.ЗаписьЖурналаРегистрации() для фиксации всех значимых событий: создания фоновых заданий, их успешного выполнения, ошибок, а также фактов, когда документ не прошел проверку актуальности. Это поможет вам отлаживать систему и оперативно реагировать на проблемы.Асинх и Ждать), которые позволяют выполнять код в многопоточном режиме, не блокируя основной поток выполнения. Хотя их семантика отличается от фоновых заданий (они скорее для выполнения длительных операций без зависания интерфейса), в некоторых случаях их можно рассматривать для частичной асинхронности. Однако, для гарантированного выполнения после фиксации транзакции, фоновые задания или планы обмена остаются предпочтительными.Выбирая между фоновыми заданиями и планами обмена, ориентируйтесь на объем документов, частоту их проведения и требования к надежности. Для небольших и средних систем фоновых заданий, как правило, достаточно. Для высоконагруженных систем или там, где требуется особо надежная система очередей, планы обмена могут быть более подходящим выбором. В любом случае, всегда выносите действия, зависящие от успешности транзакции, за ее пределы, используя асинхронные механизмы 1С.