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