Каждый разработчик 1С рано или поздно сталкивается с системным сообщением: «В данной транзакции уже происходили ошибки». В профессиональной среде эту проблему часто называют ошибкой «сломанной», «отравленной» или «испорченной» транзакции (состояние Rollback-Only). Данный сбой ставит в тупик многих специалистов, так как платформа указывает на строку кода, которая сама по себе абсолютно корректна. В этой статье мы подробно разберем природу этого явления, выясним, почему замена даты документа временно маскирует проблему, и научимся находить истинную причину сбоя с помощью отладчика и технологического журнала.
Для того чтобы понять, почему возникает эта ошибка, нам необходимо обратиться к архитектуре взаимодействия платформы «1С:Предприятие» и систем управления базами данных (СУБД), такими как MS SQL Server или PostgreSQL. Транзакция — это неделимая последовательность операций над базой данных. Она должна либо выполниться полностью (коммит), либо полностью отмениться (откат), сохраняя целостность данных.
Когда транзакция запущена (неважно, явно через код НачатьТранзакцию() или неявно при записи/проведении документа), СУБД берет под жесткий контроль все дисковые и блокировочные операции. Если внутри транзакции происходит любая критическая ошибка СУБД — например, конфликт блокировок, нарушение уникальности индексов, запись в несуществующую таблицу или попытка проведения документа по регистрам с ошибкой контроля остатков — СУБД мгновенно помечает текущую транзакцию как нежизнеспособную (Rollback-Only).
Платформа 1С транслирует эту ошибку в код. Если разработчик обернул опасный участок кода в конструкцию Попытка...Исключение, то платформа перехватывает первичный сбой, позволяя коду выполняться дальше. Однако СУБД уже приняла решение: данная транзакция никогда не будет зафиксирована. Любое последующее обращение к базе данных (даже простой запрос на чтение или получение представления ссылки) в рамках этой транзакции пресекается СУБД на корню. Платформа 1С реагирует на это повторное обращение выводом сообщения: «В данной транзакции уже происходили ошибки».
Таким образом, строка кода, на которой спотыкается программа, невиновна. Настоящая ошибка произошла раньше, была «замята» блоком Попытка...Исключение, но бесследно для СУБД не прошла.
Рассмотрим фрагмент кода, который привел автор темы на форуме:
Попытка
РезультатЗапроса = Запрос.Выполнить();
Исключение
ТекстСообщения = "Не удалось выполнить запрос по остаткам";
Возврат Ложь;
КонецПопытки;
Автор пишет, что документ «МЗ_ВнутреннееПеремещение» при проведении вываливается в исключение именно на строке РезультатЗапроса = Запрос.Выполнить(). Однако при переносе даты документа на другую (например, с 06.10.18 на 10.11.18) документ проводится без нареканий.
Проанализируем эту ситуацию. Смена даты меняет контекст данных, к которым обращается документ при проведении. На дату 06.10.18 в базе данных происходило некое событие, порождавшее первичную ошибку. Это могло быть:
Когда первичный сбой происходил, транзакция помечалась как «сломанная». Далее код управления доходил до выполнения запроса остатков. Из-за поврежденного статуса транзакции метод Запрос.Выполнить() завершался аварийно. Конструкция Попытка перехватывала эту ошибку, но исправить ситуацию уже не могла. При смене даты на ноябрь условия для первичного сбоя исчезли, транзакция осталась «чистой», и запрос выполнился штатно.
Поскольку сама ошибка указывает лишь на следствие, нам необходимо раскопать первопричину. Сделать это можно тремя надежными способами.
Обычный запуск отладки не останавливает выполнение на ошибках, которые обернуты в блок Попытка...Исключение. Нам нужно заставить конфигуратор реагировать на абсолютно все исключительные ситуации, даже если они обрабатываются кодом.
Выполним следующие шаги:
Теперь при проведении документа отладчик остановит исполнение кода не на строке РезультатЗапроса = Запрос.Выполнить(), а гораздо раньше — непосредственно в момент возникновения первого сбоя СУБД. Это может быть подписка на событие, модуль набора записей регистра или общий модуль проведения. Мы увидим реальный текст системной ошибки (например, деление на ноль, нарушение уникальности ключа или блокировка).
Если ошибка воспроизводится только на рабочем сервере под нагрузкой и подключить отладчик напрямую невозможно, на помощь приходит Технологический журнал (ТЖ) 1С. Мы можем настроить его так, чтобы он фиксировал все исключительные ситуации.
Для этого в каталоге настроек ТЖ (обычно это C:\Program Files\1cv8\conf\ на сервере) создадим или отредактируем файл logcfg.xml следующим образом:
<config xmlns="http://v8.1c.ru/v8/tech-log">
<log location="C:\v8logs\exceptions" history="2">
<event>
<eq property="Name" value="EXCP"/>
</event>
<property name="all"/>
</log>
</config>
После активации этой настройки платформа начнет записывать все исключения (событие EXCP) в указанную папку. Нам останется лишь воспроизвести ошибку, открыть лог-файл и найти записи, предшествующие сообщению «В данной транзакции уже происходили ошибки». Там будет содержаться полный стек вызовов и детальное описание первоначального исключения СУБД.
Иногда разработчики типовых конфигураций (например, БГУ или БП) предусматривают запись первичных ошибок в Журнал регистрации внутри блока Исключение, прежде чем вернуть управление. Проанализируем Журнал регистрации, установив фильтр по нашему пользователю и ограничив интервал времени секундами проведения документа. Возможно, там уже записано готовое сообщение об ошибке проведения по конкретному регистру.
Чтобы не плодить подобные трудночитаемые ошибки в собственном коде, необходимо строго следовать стандартам разработки, которые рекомендует фирма «1С».
Правило 1. Запрет подавления транзакционных ошибок.
Если вы используете блок Попытка...Исключение внутри транзакции (например, в обработчиках ПередЗаписью, ПриЗаписи, ОбработкаПроведения) и в блоке Попытка происходит любое обращение к базе данных, вы обязаны гарантировать прерывание транзакции в случае неудачи. Заминать ошибку и возвращать Ложь нельзя!
Посмотрим на пример неправильного кода:
// НЕПРАВИЛЬНО! Так делать нельзя!
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Попытка
НаборЗаписей = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
// ... заполнение и запись набора
НаборЗаписей.Записать();
Исключение
// Ошибка перехвачена, но транзакция уже сломана СУБД!
ЗаписатьВЛог("Не удалось записать цены");
// Код продолжает выполнение, провоцируя последующий крах транзакции
КонецПопытки;
КонецПроцедуры
Правильный вариант перехвата ошибок внутри транзакции всегда должен содержать оператор ВызватьИсключение, чтобы платформа корректно инициировала откат всей транзакции:
// ПРАВИЛЬНО!
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Попытка
НаборЗаписей = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
// ... заполнение и запись набора
НаборЗаписей.Записать();
Исключение
// Фиксируем информацию для администратора
ЗаписатьВЛог("Критическая ошибка записи цен: " + ОписаниеОшибки());
// Прерываем транзакцию и показываем пользователю исходную ошибку
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Правило 2. Валидация вместо попыток записи.
Вместо того чтобы пытаться записать объект в базу данных и перехватывать ошибку в случае неудачи, мы должны предварительно проверять все условия программно. Например, вместо попытки проведения документа по регистру с надеждой отловить ошибку нехватки остатков через Попытка, следует предварительно выполнить запрос к остаткам и убедиться, что ресурсов достаточно.
Выяснив причину ошибки «В данной транзакции уже происходили ошибки», мы понимаем, что она является лишь защитным механизмом СУБД, сигнализирующим о попытке работать со сломанной транзакцией. Попытка проигнорировать первичный сбой с помощью конструкции Попытка...Исключение приводит к потере контекста ошибки. Использование правильной настройки отладчика с остановкой по обрабатываемым исключениям позволяет за пару минут локализовать исходное место сбоя и исправить логику работы программы.