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