Как проверить, что вложенная транзакция отменена, и избежать ошибки «В данной транзакции уже происходили ошибки!»

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

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

Почему происходит ошибка: миф о вложенных транзакциях в 1С

Начнем с фундаментального правила платформы 1С:Предприятие: вложенных транзакций на уровне СУБД не существует. Платформа лишь имитирует вложенность программно с помощью внутреннего счетчика транзакций.

Рассмотрим, как работает этот механизм по шагам:

  1. При первом вызове метода НачатьТранзакцию() платформа открывает реальную физическую транзакцию в СУБД (например, в MS SQL Server или PostgreSQL). Счетчик глубины транзакций становится равным 1.
  2. При повторном (вложенном) вызове метода НачатьТранзакцию() СУБД ничего не знает о новой транзакции. Платформа просто увеличивает внутренний счетчик глубины до 2.
  3. Если на внутреннем уровне вызывается ЗафиксироватьТранзакцию(), платформа уменьшает счетчик до 1. Реальной фиксации данных в базе на этом этапе не происходит.
  4. Если же на внутреннем уровне вызывается метод ОтменитьТранзакцию() или происходит любая СУБД-ошибка (например, блокировка или нарушение уникальности), вся транзакция целиком помечается платформой как «сломанная» (Rollback-Only).

В состоянии Rollback-Only транзакция все еще считается активной (метод ТранзакцияАктивна() упорно продолжает возвращать Истина), однако СУБД больше не примет ни одной команды. Любая попытка прочитать данные (запрос, поиск по коду, получение объекта) или записать их мгновенно вызовет исключение «В данной транзакции уже происходили ошибки!». Единственное действие, которое разрешено выполнять со «сломанной» транзакцией — это ее полная отмена.

Способ 1. Косвенный программный тест транзакции (тест на Rollback-Only)

Поскольку платформа не предоставляет прямого метода вроде ТранзакцияСломана(), мы можем написать собственную функцию для проверки состояния транзакции. Для этого выполним сверхлегкий запрос к базе данных внутри блока попытки. Если запрос падает с ошибкой — транзакция гарантированно находится в состоянии отмены.

Давайте проанализируем пример такой функции:


Функция ТранзакцияСломана() Экспорт
    
    Если Не ТранзакцияАктивна() Тогда
        Возврат Ложь;
    КонецЕсли;
    
    Запрос = Новый Запрос("ВЫБРАТЬ 1");
    Попытка
        Запрос.Выполнить();
        Возврат Ложь; // Транзакция жива и работоспособна
    Исключение
        Возврат Истина; // Транзакция в режиме Rollback-Only
    КонецПопытки;
    
КонецФункции

Используя эту функцию, мы можем безопасно ветвить выполнение логики в коде из Сообщения 0:


&НаСервереБезКонтекста
Процедура Команда1НаСервере()
    
    НачатьТранзакцию();
    НачатьТранзакцию();
    
    ОтменитьТранзакцию(); // Ломаем внутреннюю транзакцию
    
    // Проверяем состояние перед обращением к БД
    Если Не ТранзакцияСломана() Тогда
        Сообщить(Строка(Справочники.Валюты.НайтиПоКоду("643")));
    Иначе
        Сообщить("Выполнение операции отменено: транзакция неактивна или повреждена.");
    КонецЕсли;
    
КонецПроцедуры

Способ 2. Полный сброс транзакций в цикле

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

Мы можем программно вызывать метод ОтменитьТранзакцию() в цикле до тех пор, пока счетчик вложенности не обнулится, и функция ТранзакцияАктивна() не вернет значение Ложь. Посмотрим, как это выглядит на практике:


&НаСервереБезКонтекста
Процедура БезопасныйОткатТранзакции()
    
    НачатьТранзакцию();
    НачатьТранзакцию();
    
    // Какая-то логика привела к необходимости отмены
    ОтменитьТранзакцию();
    
    // Сворачиваем стек транзакций до самого верха
    Пока ТранзакцияАктивна() Цикл
        Попытка
            ОтменитьТранзакцию();
        Исключение
            // Обрабатываем исключение, если транзакция неявная
            Прервать;
        КонецПопытки;
    КонецЦикла;
    
    // Теперь база данных снова доступна для обычных запросов вне транзакции
    Сообщить(Строка(Справочники.Валюты.НайтиПоКоду("643")));
    
КонецПроцедуры

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

Способ 3. Использование фоновых заданий для независимых операций

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

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

Разберем пример реализации записи лога через фоновое задание:


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

Внутри процедуры ЗаписатьОшибкуВРегистрФоновымЗаданием будет открыта своя транзакция, которая успешно запишет данные, несмотря на то, что в основном сеансе транзакция полностью откатывается.

Стандарты разработки 1С для предотвращения ошибок

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

  1. Логическая парность кода: Каждый вызов НачатьТранзакцию() должен гарантированно завершаться либо вызовом ЗафиксироватьТранзакцию(), либо ОтменитьТранзакцию(). Недопустимо открывать транзакцию внутри процедуры, а закрывать ее за ее пределами.
  2. Правильная структура блоков Попытка-Исключение: Если вы открываете явную транзакцию и внутри нее используете перехват ошибок, блок исключения должен выглядеть строго определенным образом. Рассмотрим классический шаблон:

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

Ни в коем случае нельзя «замалчивать» ошибку в блоке Исключение, оставляя транзакцию активной. Это гарантированно приведет к ошибкам Rollback-Only на более высоких уровнях стека вызовов.

← На главную