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