Работа с транзакциями в 1С:Предприятии — одна из самых ответственных задач для разработчика. Часто возникает ситуация, когда нам необходимо программно провести один или несколько документов внутри одной общей транзакции. Однако стандартная конструкция Попытка...Исключение при наличии открытой транзакции может вести себя непредсказуемо. Разберем подробно, почему возникает ошибка «В данной транзакции уже происходили ошибки» и как правильно организовать код, чтобы избежать «вакханалии» с переменными и стабильностью системы.
Проанализируем ситуацию: вы открываете транзакцию методом НачатьТранзакцию(), затем внутри блока Попытка вызываете проведение документа. Если в процессе проведения (на уровне СУБД или платформы) происходит ошибка, транзакция переходит в состояние Transaction Doomed (обреченная транзакция). Это специфическое состояние, при котором база данных блокирует любые дальнейшие операции в рамках этого соединения до тех пор, пока не будет выполнен полный откат (Rollback).
Рассмотрим основные причины, по которым транзакция мгновенно помечается как неисправимая:
Записать(РежимЗаписиДокумента.Проведение) открывает свою внутреннюю системную транзакцию. Если она завершается аварийно, она «отравляет» вашу внешнюю транзакцию.Главная ловушка заключается в том, что в блоке Исключение любое обращение к базе данных (даже простое чтение свойства объекта или вызов функции ТранзакцияАктивна()) может спровоцировать новое исключение, так как система видит «битую» транзакцию и не дает совершить ни одного действия, кроме отката.
Для того чтобы корректно обработать ошибку и не потерять контроль над выполнением программы, рассмотрим классический шаблон работы с транзакцией. Выясним причину, почему стандартные подходы иногда подводят.
Посмотрим на пример кода, который считается эталонным:
НачатьТранзакцию();
Попытка
// Выполняем действия, например, проведение документов
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
// ВАЖНО: Проверяем активность транзакции перед откатом
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
// Передаем ошибку выше или записываем в лог
ВызватьИсключение;
КонецПопытки;
Почему это может не сработать? В новых версиях платформы (начиная с 8.3.15) при возникновении серьезных ошибок на уровне SQL сам вызов функции ТранзакцияАктивна() в блоке исключения может вызвать ошибку «В данной транзакции уже происходили ошибки». Это происходит потому, что функция пытается опросить состояние контекста БД, который уже заблокирован.
Проанализируем ситуацию, когда в режиме отладки переменные кажутся «неопределенными» или их просмотр вызывает новые ошибки. Это происходит из-за того, что отладчик при наведении курсора на переменную (например, на ДокументОбъект) пытается вычислить её состояние, обращаясь к серверу приложений. Так как транзакция уже «сломана», сервер возвращает ошибку на любой запрос отладчика. В итоге разработчик видит пустые значения переменных, хотя в оперативной памяти они существуют.
Если ваша задача — гарантировать, что либо все документы проведутся, либо ни один, придерживайтесь следующих правил:
Попытка...Исключение внутри одной транзакции, если вы не планируете игнорировать ошибки (что в случае с проведением документов крайне опасно).Исключение не делайте запросов к базе. Единственное допустимое действие — отмена транзакции и регистрация ошибки.Рассмотрим более надежный вариант реализации:
ОписаниеОшибки = "";
НачатьТранзакцию();
Попытка
// Проводим первый документ
Документ1.Записать(РежимЗаписиДокумента.Проведение);
// Проводим второй документ
Документ2.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
ОписаниеОшибки = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
// Если транзакция была начата, ее НУЖНО отменить.
// Если возникнет ошибка на ОтменитьТранзакцию(), она будет обработана внешним уровнем.
Попытка
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
Исключение
// Здесь мы уже ничего не можем сделать с БД
КонецПопытки;
ЗаписьЖурналаРегистрации("ПроведениеДокументов", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки);
ВызватьИсключение "Ошибка при проведении: " + ОписаниеОшибки;
КонецПопытки;
Выясним, какие еще факторы могут влиять на поведение системы:
НачатьТранзакцию() и ЗафиксироватьТранзакцию() должны находиться строго в рамках одного серверного вызова.Подводя итог: если вы столкнулись с ошибкой «В данной транзакции уже происходили ошибки», значит, платформа уже обнаружила критический сбой в работе с данными. Ваша задача в этом случае — как можно быстрее выполнить ОтменитьТранзакцию(), не пытаясь анализировать данные или проверять свойства объектов до завершения отката.