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