Как избежать ошибки «В данной транзакции произошли уже ошибки» при записи данных в регистр сведений из подписки «ПриЗаписи»?

Программист 1С v8.3 (Управляемые формы) 1С:Зарплата и Управление Персоналом IT и автоматизация бизнеса
← На главную

В процессе разработки на платформе 1С:Предприятие, особенно при использовании механизмов подписок на события, мы часто сталкиваемся с необходимостью выполнения дополнительных действий при изменении стандартных объектов. Одним из таких сценариев является запись данных в собственный регистр сведений при сохранении объекта, например, при записи сотрудника. Однако, при попытке такой записи, особенно если внутри возникает ошибка, мы можем получить довольно неприятное сообщение: «В данной транзакции произошли уже ошибки». Давайте подробно разберем, почему возникает эта ошибка, и какие существуют подходы к ее устранению.

Анализируем исходную проблему

Предположим, у нас есть подписка на событие ПриЗаписи объекта Справочник.Сотрудники, и в обработчике этой подписки мы пытаемся записать данные в собственный регистр сведений РегистрыСведений.МойРегистр. Код обработчика подписки выглядит примерно так:


// Обработчик события ПриЗаписи
Процедура МойРегистрПриЗаписиСотрудника(Источник, Отказ, Замещение) Экспорт
    Если Отказ ИЛИ Загрузка Тогда
        Возврат;
    КонецЕсли;

    // Устанавливаем привилегированный режим для записи
    УстановитьПривилегированныйРежим(Истина);

    Попытка
        МЗ = РегистрыСведений.МойРегистр.СоздатьМенеджерЗаписи();
        // Предположим, СтруктураЗаписи содержит данные для МЗ
        ЗаполнитьЗначенияСвойств(МЗ, СтруктураЗаписи);
        МЗ.Записать();
    Исключение
        ТекстСообщения = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
        ЗаписьЖурналаРегистрации("МойРегистр.СоздатьЗапись",УровеньЖурналаРегистрации.Ошибка,,,ТекстСообщения);
        Сообщить("Произошла ошибка при записи в МойРегистр: " + ТекстСообщения);
    КонецПопытки;

    УстановитьПривилегированныйРежим(Ложь);
КонецПроцедуры

Мы видим, что для записи в регистр используется менеджер записи МЗ, а сама операция записи обернута в блок Попытка...Исключение для обработки возможных ошибок. Также для обхода прав доступа используется УстановитьПривилегированныйРежим(Истина). При тестовом сохранении сотрудника в пользовательском режиме, если запись в регистр проходит успешно, всё работает без проблем. Однако, как только возникает ошибка внутри блока Попытка (например, из-за незаполненного обязательного измерения или ресурса регистра), несмотря на то, что мы перехватываем исключение, дальнейшее выполнение кода в типовой конфигурации приводит к ошибке «В данной транзакции произошли уже ошибки».

Эта ошибка обычно проявляется при попытке выполнить метод БлокировкаДанных.Заблокировать(), который находится в типовом коде, например, в общем модуле СотрудникиФормыБазовый.ЗаписатьТекущиеДанные() или аналогичном. Давайте посмотрим, почему так происходит.

Выясняем причину: Неявные транзакции и их состояние

Ключ к пониманию этой проблемы кроется в механизме транзакций платформы 1С:Предприятие. Когда мы записываем любой объект (например, справочник, документ) в базу данных, платформа автоматически начинает неявную транзакцию. Все обработчики событий, такие как ПриЗаписи, которые вызываются в процессе сохранения объекта, выполняются внутри этой неявной транзакции.

Транзакция — это атомарная операция: либо все ее изменения фиксируются (ЗафиксироватьТранзакцию()), либо все отменяются (ОтменитьТранзакцию()). В случае неявной транзакции платформы, она либо будет успешно зафиксирована после завершения всех операций записи, либо отменена, если возникнет критическая ошибка.

Теперь рассмотрим, что происходит, когда ошибка возникает внутри транзакции:

  1. Начало неявной транзакции: Платформа начинает транзакцию при вызове метода Записать() для объекта Сотрудник.
  2. Вызов подписки: В процессе записи объекта вызывается наш обработчик подписки МойРегистрПриЗаписиСотрудника.
  3. Ошибка внутри Попытка...Исключение: Когда метод МЗ.Записать() вызывает ошибку (например, нарушение целостности данных, попытка записать Null в поле, не допускающее Null), платформа фиксирует, что внутри текущей транзакции произошла ошибка, связанная с базой данных.
  4. Состояние транзакции "только для отмены": Это критический момент. Несмотря на то, что мы перехватываем исключение с помощью Попытка...Исключение, сама транзакция, в рамках которой выполнялась операция с базой данных, помечается платформой как "только для отмены". Это означает, что она не может быть успешно зафиксирована в дальнейшем.
  5. Повторное обращение к базе данных: После того, как наша подписка завершилась (и мы перехватили исключение), типовой код продолжает свою работу. Он может попытаться выполнить другие операции с базой данных, например, установить блокировку данных с помощью БлокировкаДанных.Заблокировать(). Поскольку текущая транзакция уже помечена как "только для отмены", любая попытка выполнить новую операцию с базой данных (даже просто чтение или установка блокировки) внутри этой "поврежденной" транзакции приведет к ошибке «В данной транзакции произошли уже ошибки». Платформа просто не позволит выполнять дальнейшие операции с БД в транзакции, которая уже находится в неисправимом состоянии.

Утверждение автора, что ТранзакцияАктивна() = ИСТИНА, но при этом нет явного НачатьТранзакцию(), лишь подтверждает наличие неявной транзакции платформы. Попытка вызвать ОтменитьТранзакцию() или ЗафиксироватьТранзакцию() в таком состоянии также, скорее всего, приведет к ошибке, так как платформа сама управляет этой транзакцией и не ожидает явных команд от разработчика в таком контексте.

По сути, механизм Попытка...Исключение позволяет нам перехватить ошибку и продолжить выполнение кода в модуле, но он не "чинит" состояние транзакции, если ошибка была связана с базой данных.

Эффективные подходы к решению проблемы

Теперь, когда мы понимаем корень проблемы, давайте рассмотрим, как ее можно решить. Есть два основных подхода, в зависимости от того, является ли прерывание записи сотрудника приемлемым или нет.

1. Предотвращение ошибок базы данных (Предварительная валидация)

Это наиболее предпочтительный и "аккуратный" способ решения, особенно если мы хотим, чтобы запись сотрудника продолжалась, даже если наш дополнительный регистр не может быть записан по какой-то причине (или если мы хотим максимально избежать ошибок). Идея заключается в том, чтобы проверять все условия для успешной записи в регистр ДО того, как мы попытаемся вызвать МЗ.Записать().

Если данные не соответствуют условиям, мы можем либо отказаться от записи в наш регистр, либо выдать пользователю сообщение об ошибке без повреждения основной транзакции.

  1. Валидация данных: Перед вызовом МЗ.Записать() необходимо убедиться, что все обязательные поля регистра заполнены корректными значениями, соответствуют типам данных и не нарушают других ограничений базы данных (например, уникальность).

    Например, если у вас есть обязательное измерение, которое может быть не заполнено:

    
    // Предположим, СтруктураЗаписи должна содержать поле "КлючевоеИзмерение"
    Если НЕ СтруктураЗаписи.Свойство("КлючевоеИзмерение") ИЛИ НЕ ЗначениеЗаполнено(СтруктураЗаписи.КлючевоеИзмерение) Тогда
        Сообщить("Не заполнено ключевое измерение для записи в МойРегистр. Запись в регистр отменена.", СтатусСообщения.Внимание);
        Возврат; // Выходим из обработчика без попытки записи
    КонецЕсли;
    
  2. Удаление блока Попытка...Исключение: Если вы уверены, что все возможные ошибки базы данных предотвращены предварительной валидацией, блок Попытка...Исключение становится избыточным для перехвата ошибок БД. Ошибки, которые не связаны с базой данных (например, ошибки логики), можно обрабатывать локально, но если вызов МЗ.Записать() может повлечь ошибку БД, лучше ее предотвратить.

    Модифицированный обработчик будет выглядеть так:

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

Преимущество этого подхода: Основная транзакция записи сотрудника не повреждается, даже если мы не смогли записать данные в наш вспомогательный регистр. Это позволяет сохранить атомарность основной операции, но при этом сообщить пользователю о проблемах с дополнительными данными.

2. Перевыброс исключения (Принудительная отмена основной транзакции)

Если предотвратить ошибку невозможно или нецелесообразно, и мы считаем, что неудача записи в наш регистр должна полностью отменить всю операцию сохранения сотрудника, тогда мы можем использовать ВызватьИсключение в блоке Исключение.

  1. Простота реализации: Этот подход технически проще, так как не требует детальной предварительной валидации.

    Модифицированный обработчик:

    
    Процедура МойРегистрПриЗаписиСотрудника(Источник, Отказ, Замещение) Экспорт
        Если Отказ ИЛИ Загрузка Тогда
            Возврат;
        КонецЕсли;
    
        УстановитьПривилегированныйРежим(Истина);
    
        Попытка
            МЗ = РегистрыСведений.МойРегистр.СоздатьМенеджерЗаписи();
            // Предполагаем, что СтруктураЗаписи заполнена
            ЗаполнитьЗначенияСвойств(МЗ, СтруктураЗаписи);
            МЗ.Записать();
        Исключение
            ТекстСообщения = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
            ЗаписьЖурналаРегистрации("МойРегистр.СоздатьЗапись",УровеньЖурналаРегистрации.Ошибка,,,ТекстСообщения);
            Сообщить("Произошла критическая ошибка при записи в МойРегистр. Отмена сохранения сотрудника.", СтатусСообщения.Ошибка);
            // **Ключевое изменение: перевыбрасываем исключение**
            ВызватьИсключение "Не удалось записать данные в регистр 'МойРегистр'. Подробности в журнале регистрации. " + ТекстСообщения;
        КонецПопытки;
    
        УстановитьПривилегированныйРежим(Ложь);
    КонецПроцедуры
    

Преимущество этого подхода: Гарантирует атомарность всей операции. Если хоть что-то пошло не так при записи в наш регистр, то и запись основного объекта Сотрудник будет отменена. Платформа корректно отменит всю неявную транзакцию. Пользователь увидит сообщение об ошибке, и изменения не будут сохранены.

Недостаток: Прерывает основную операцию записи сотрудника, что не всегда желательно. Например, если наш регистр второстепенный, а запись сотрудника важнее, этот подход не подойдет.

3. Избегание явного управления транзакциями в подписках

Важно помнить, что в обработчиках событий, которые вызываются в рамках неявных транзакций платформы (таких как ПриЗаписи, ПередЗаписью и т.д.), не рекомендуется использовать явные команды управления транзакциями: НачатьТранзакцию(), ЗафиксироватьТранзакцию(), ОтменитьТранзакцию(). Эти команды предназначены для создания собственных, явно управляемых транзакций. Их использование внутри уже активной неявной транзакции платформы может привести к непредсказуемому поведению и ошибкам, особенно если не соблюдать строгие правила вложенности и завершения транзакций.

В случае возникновения ошибки базы данных внутри такой неявной транзакции, как мы уже выяснили, транзакция становится "поврежденной". Попытка ее отменить или зафиксировать с помощью явных команд скорее всего вызовет ту же ошибку "В данной транзакции произошли уже ошибки" или другие проблемы.

Особенности использования привилегированного режима

Не забудем про УстановитьПривилегированныйРежим(Истина). Этот метод позволяет выполнять операции с базой данных без проверки прав текущего пользователя. Это полезно для служебных операций, которые должны быть выполнены независимо от прав пользователя. Однако, он не влияет на механизм транзакций и не предотвращает возникновение ошибки "В данной транзакции произошли уже ошибки", если в транзакции уже произошла ошибка базы данных. Всегда помните о необходимости снять привилегированный режим с помощью УстановитьПривилегированныйРежим(Ложь) после выполнения необходимых операций, чтобы не нарушать механизмы безопасности платформы.

Заключение

Ошибка «В данной транзакции произошли уже ошибки» в 1С при работе с подписками на события является следствием повреждения неявной транзакции платформы из-за возникновения ошибки, связанной с базой данных. Перехват исключения с помощью Попытка...Исключение предотвращает падение всего кода, но не "восстанавливает" состояние транзакции, делая ее непригодной для дальнейших операций с БД.

Наиболее надежным и рекомендуемым решением является предварительная валидация данных. Мы должны стремиться к тому, чтобы наша запись в регистр (или любая другая операция с БД в подписке) была максимально "безошибочной" на уровне данных. Если же возникновение ошибки базы данных при записи в наш регистр должно полностью прервать основную операцию, то перевыброс исключения (ВызватьИсключение) является корректным способом информирования платформы о необходимости отменить всю транзакцию.

Всегда помните о поведении транзакций в 1С:Предприятие, особенно при работе с обработчиками событий, которые выполняются в неявных транзакциях. Это поможет вам избежать многих сложностей и создавать более надежный и стабильный код.

← На главную