При разработке в 1С:Предприятии мы часто сталкиваемся с необходимостью обрабатывать ошибки, особенно когда речь идет о записи данных в базу. Одной из распространенных проблем является попытка записать информацию об ошибке в тот же документ или объект, в рамках транзакции которого эта ошибка и произошла. Пользователи форумов и разработчики сталкиваются с сообщением: "В данной транзакции уже произошла ошибка...". Давайте подробно разберем, почему возникает эта ситуация и как ее эффективно решить.
Представим сценарий: мы создаем основной документ, который перед записью или в процессе проведения должен сгенерировать и записать несколько подчиненных документов. Если в процессе создания или записи любого из этих подчиненных документов возникает ошибка (например, не хватает остатков, нарушена целостность данных, или просто некорректно заполнены реквизиты), вся текущая транзакция помечается для отмены. В такой ситуации наша попытка записать текст ошибки в отдельный реквизит основного документа, находясь все еще в блоке `Исключение` этой же транзакции, будет безуспешной. Система не позволит выполнить какие-либо операции записи в рамках уже ошибочной транзакции, чтобы гарантировать целостность данных.
Для понимания сути проблемы давайте рассмотрим, как платформа 1С:Предприятие работает с транзакциями. Транзакция – это неделимая последовательность операций с базой данных, которая либо выполняется полностью (фиксируется), либо отменяется целиком (откатывается). Это обеспечивает свойство атомарности (Atomicity) ACID-свойств баз данных, гарантируя, что база данных всегда находится в согласованном состоянии.
Когда в рамках активной транзакции (начатой с помощью `НачатьТранзакцию()`) происходит какая-либо ошибка, платформа 1С немедленно помечает эту транзакцию как "ошибочную". Это означает, что дальнейшие попытки модификации или даже чтения данных из базы в контексте этой же транзакции будут запрещены. Система устанавливает внутренний флаг "Запрет успешного завершения" для текущей транзакции. Фактический откат всех изменений происходит при попытке зафиксировать транзакцию (`ЗафиксироватьТранзакцию()`) или при явном вызове `ОтменитьТранзакцию()`.
Таким образом, когда мы пытаемся в блоке `Исключение` (после того как ошибка уже произошла) записать текст ошибки в реквизит документа, мы все еще находимся в контексте той самой "сломанной" транзакции. Платформа видит это и выдает предупреждение: "В данной транзакции уже происходили ошибки", предотвращая любые дальнейшие записи, чтобы избежать потенциального нарушения целостности данных.
Чтобы эффективно управлять транзакциями и корректно обрабатывать ошибки, нам важно придерживаться нескольких ключевых правил:
Единственный надежный способ записать информацию об ошибке в реквизит документа или в любой другой объект базы данных после того, как основная транзакция была отменена, — это выполнить эту операцию в новой, независимой транзакции. Давайте разберем этот процесс по шагам.
Наш первый шаг — корректно перехватить исключение, которое возникло при работе с подчиненными документами, и отменить текущую, проблемную транзакцию. В блоке `Исключение` мы собираем всю необходимую информацию об ошибке в обычные переменные 1С (строки, числа, булевы), которые существуют в памяти и не зависят от состояния базы данных.
&НаКлиенте
Процедура ЗаписатьДокументСПодчиненными()
НачатьТранзакцию(); // Начало основной транзакции
Попытка
// Здесь мы создаем и записываем подчиненные документы
МойДокументОбъект = РеквизитФормыВЗначение("Объект"); // Если работаем в форме
// Или получаем объект по ссылке, если это серверный вызов
// МойДокументОбъект = МойДокументСсылка.ПолучитьОбъект();
// Имитация создания подчиненного документа и возможной ошибки
// ... Ваш код создания подчиненных документов ...
НовыйПодчиненныйДокумент = Документы.РеализацияТоваровУслуг.СоздатьДокумент(); // Пример
// Заполняем НовыйПодчиненныйДокумент...
// ...
НовыйПодчиненныйДокумент.Контрагент = Справочники.Контрагенты.НайтиПоКоду("000000001");
НовыйПодчиненныйДокумент.Организация = Справочники.Организации.НайтиПоКоду("000000001");
// ... и так далее ...
// Допустим, здесь возникла ошибка при записи подчиненного документа
// Например, из-за неверных данных или ошибки в обработчике ПередЗаписью
НовыйПодчиненныйДокумент.Записать(); // Здесь может произойти исключение
// Если все успешно, фиксируем основную транзакцию
ЗафиксироватьТранзакцию();
Сообщить("Основной документ и подчиненные успешно записаны.");
Исключение
ОтменитьТранзакцию(); // Отменяем основную транзакцию!
ТекстОшибки = ОписаниеОшибки();
ТекстСобытия = "При записи подчиненных документов для основного документа "
+ МойДокументОбъект.Ссылка + " произошла ошибка: " + ТекстОшибки;
// Сбор дополнительной информации для логирования
ИмяДокументаИсточника = "Документ " + МойДокументОбъект.Метаданные().Имя + " №" + МойДокументОбъект.Номер;
ПодробностиОшибки = "Попытка записи подчиненных документов не удалась. " + ТекстОшибки;
// **Внимание:** После ОтменитьТранзакцию() мы можем начать новую транзакцию.
// Передаем информацию об ошибке в отдельную процедуру или выполняем логирование здесь.
ЗаписатьОшибкуВРеквизит(МойДокументОбъект.Ссылка, ПодробностиОшибки);
Сообщить("Произошла ошибка, транзакция отменена. Информация об ошибке записана в документ.");
ВызватьИсключение; // Передаем исключение выше, если необходимо дальнейшее оповещение
КонецПопытки;
КонецПроцедуры
&НаСервере
Процедура ЗаписатьОшибкуВРеквизит(СсылкаНаОсновнойДокумент, ТекстПодробностей)
// Начинаем НОВУЮ, НЕЗАВИСИМУЮ транзакцию для записи лога
НачатьТранзакцию();
Попытка
// Получаем объект основного документа, чтобы записать в него ошибку
ОбъектМойДокумент = СсылкаНаОсновнойДокумент.ПолучитьОбъект();
Если ОбъектМойДокумент <> Неопределено Тогда
// Предположим, у нашего документа есть реквизит "ТекстОшибкиПоследнейЗаписи"
// или ТЧ для хранения логов
ОбъектМойДокумент.ТекстОшибкиПоследнейЗаписи = ТекстПодробностей;
// Если есть ТЧ для логов
// НоваяСтрокаТЧ = ОбъектМойДокумент.ЛогиОшибок.Добавить();
// НоваяСтрокаТЧ.ДатаВремя = ТекущаяДата();
// НоваяСтрокаТЧ.ОписаниеОшибки = ТекстПодробностей;
// ...
ОбъектМойДокумент.Записать(); // Записываем изменения в основной документ
КонецЕсли;
ЗафиксироватьТранзакцию(); // Фиксируем транзакцию записи лога
Исключение
ОтменитьТранзакцию(); // Если даже запись лога пошла не так, отменяем и эту транзакцию
// В этом случае, возможно, стоит записать в Журнал Регистрации или внешний файл.
ЗаписатьВЖурналРегистрации("Критическая ошибка при попытке записи лога ошибки в документ: " + ОписаниеОшибки(), УровеньЖурналаРегистрации.Ошибка);
КонецПопытки;
КонецПроцедуры
После того как `ОтменитьТранзакцию()` была вызвана, контекст предыдущей (ошибочной) транзакции полностью очищен. Это позволяет нам начать новую транзакцию, которая будет полностью независима от предыдущей. В рамках этой новой транзакции мы можем безопасно выполнить операции записи, например, обновить реквизит нашего основного документа или добавить запись в специальный регистр сведений, предназначенный для хранения логов ошибок.
Почему это работает? Потому что новая транзакция не имеет никакого отношения к состоянию предыдущей. Для нее нет флага "Запрет успешного завершения", и она воспринимается системой как совершенно новая, чистая операция.
Этот подход позволяет нам сохранить важную информацию об ошибке, не нарушая целостности данных, поскольку основная операция, приведшая к ошибке, была полностью отменена.
Помимо записи ошибок в реквизиты документа или специальные регистры сведений, мы можем рассмотреть и другие методы логирования, которые могут быть более подходящими в зависимости от масштаба и требований проекта.
Это самый простой и часто используемый способ логирования. Платформа 1С:Предприятие имеет встроенный механизм журнала регистрации, куда можно записывать события различных типов, включая ошибки. Это отличный способ для общего логирования, который не требует изменения структуры метаданных (добавления реквизитов или регистров).
// В блоке Исключение, после ОтменитьТранзакцию()
ЗаписатьВЖурналРегистрации(
"Ошибка при создании подчиненных документов",
УровеньЖурналаРегистрации.Ошибка,
Метаданные.Документы.МойДокумент,
МойДокументОбъект.Ссылка,
"Подробности: " + ТекстОшибки + ", документ-источник: " + ИмяДокументаИсточника
);
Преимущества: Простота использования, централизованное хранение логов, возможность просмотра через стандартные средства 1С.
Недостатки: Менее гибкое хранение (сложно хранить произвольную структуру данных), может быть менее удобно для специфического анализа изнутри конфигурации.
Для очень больших объемов логов или для сценариев, где требуется независимость от базы данных 1С, можно настроить запись ошибок в текстовые файлы на сервере. Это особенно актуально для интеграционных решений или фоновых заданий.
// В блоке Исключение, после ОтменитьТранзакцию()
ИмяФайлаЛога = "C:\Logs\ErrorLog_" + Формат(ТекущаяДата(), "ДФ=yyyyMMdd") + ".txt";
ТекстЛога = Формат(ТекущаяДата(), "ДЛФ=ДВ") + " | Ошибка: " + ТекстОшибки + " | Документ: " + ИмяДокументаИсточника;
Попытка
ТекстовыйДокумент = Новый ТекстовыйДокумент();
ТекстовыйДокумент.Прочитать(ИмяФайлаЛога, КодировкаТекста.UTF8); // Пытаемся прочитать существующий
Исключение
// Если файла нет, создастся новый при записи
КонецПопытки;
ТекстовыйДокумент.ДобавитьСтроку(ТекстЛога);
Попытка
ТекстовыйДокумент.Записать(ИмяФайлаЛога, КодировкаТекста.UTF8);
Исключение
ЗаписатьВЖурналРегистрации("Не удалось записать в файл лога: " + ОписаниеОшибки(), УровеньЖурналаРегистрации.Ошибка);
КонецПопытки;
Преимущества: Высокая производительность, независимость от СУБД 1С, возможность использовать сторонние утилиты для анализа логов.
Недостатки: Требует организации доступа к файловой системе, нет встроенных средств просмотра и фильтрации в 1С.
В сложных сценариях, когда логирование ошибки само по себе требует выполнения значительных действий (например, отправка уведомлений, формирование отчета об ошибке, запуск других процессов), можно инициировать фоновое задание. Фоновое задание будет выполняться в собственной, независимой транзакции и сможет безопасно обработать и записать информацию об ошибке.
// В блоке Исключение, после ОтменитьТранзакцию()
ПараметрыФоновогоЗадания = Новый Структура();
ПараметрыФоновогоЗадания.Вставить("ТекстОшибки", ТекстОшибки);
ПараметрыФоновогоЗадания.Вставить("СсылкаДокументаИсточника", МойДокументОбъект.Ссылка);
ФоновыеЗадания.Выполнить("Модули.МодульОбработкиОшибок.ОбработатьОшибкаВФоне", ПараметрыФоновогоЗадания);
Преимущества: Асинхронная обработка, не блокирует пользователя, выполнение сложных сценариев логирования.
Недостатки: Добавляет сложность в архитектуру, требует написания дополнительных модулей для фоновых заданий, отложенное выполнение.
Давайте еще раз подчеркнем некоторые критически важные аспекты, которые помогут нам избежать новых проблем при логировании ошибок:
Как мы уже упоминали, после возникновения ошибки в транзакции, любые обращения к базе данных в ее контексте потенциально опасны. Это включает в себя неявные операции, такие как получение представления ссылки. Если в блоке `Исключение` мы попытаемся получить, например, `МойДокументОбъект.Ссылка.Представление` и это действие потребует обращения к базе данных в рамках уже "сломанной" транзакции (что бывает при первом обращении к представлению для еще не записанных ссылок или при проблемах с кешем), мы можем получить новое исключение или ту же самую ошибку "В данной транзакции уже происходила ошибка!". Чтобы этого избежать, лучше всего:
Даже после того, как мы отменили транзакцию и записали информацию об ошибке, часто полезно передать исходное исключение (или новое, более информативное) дальше по стеку вызовов с помощью `ВызватьИсключение`. Это позволяет более высоким уровням кода или обработчикам ошибок платформы получить информацию о проблеме, что может быть критично для общей диагностики, а также для отображения сообщений пользователю в интерфейсе.
Например, если код находится в серверной процедуре, вызванной с клиента, `ВызватьИсключение` гарантирует, что сообщение об ошибке будет доставлено до клиента и может быть показано пользователю.
В первоначальном запросе автор упомянул, что цель записи ошибки в реквизит документа — передать информацию о причине непроведения документа в другую базу данных. Предложенное нами решение с записью ошибки в новой, независимой транзакции идеально подходит для этого сценария. Если у нас есть основной документ, который порождает 6 подчиненных при перегрузке из одной базы в другую, и один из подчиненных документов вызывает ошибку, мы можем:
Это позволяет сохранить актуальный статус и причину сбоя непосредственно в документе-источнике, обеспечивая прозрачность и упрощая отладку интеграционных процессов.
Таким образом, хотя платформа 1С строго следит за целостностью данных в транзакциях, понимание ее механики позволяет нам найти элегантные и надежные решения для логирования ошибок, даже в самых сложных сценариях. Ключ к успеху — всегда помнить о контексте транзакции и выполнять операции логирования вне контекста уже ошибочной транзакции.