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