Почему проведение документа в блоке "Попытка" внутри транзакции приводит к ошибке "В данной транзакции уже происходили ошибки!" и как это исправить?

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

Многие разработчики 1С сталкиваются с неочевидным поведением при попытке провести документ в блоке Попытка ... Исключение, который в свою очередь находится внутри явной транзакции НачатьТранзакцию() ... ЗафиксироватьТранзакцию(). Часто такая конструкция приводит к ошибке "В данной транзакции уже происходили ошибки!". Давайте вместе разберем эту ситуацию, выясним причину возникновения ошибки и предложим эффективные решения.

Рассмотрим типовой сценарий, который вызывает данную проблему. Представим, что у нас есть код, который создает и пытается провести документ:


НачатьТранзакцию();
Попытка
    ДокументОбъект = Документы.ПеремещениеТоваров.СоздатьДокумент();
    //***** заполнение реквизитов документа *****
    ДокументОбъект.ДополнительныеСвойства.Вставить("СШПНеобрабатывать", Истина);
    Попытка
        ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
    Исключение
        ДокументОбъект.ОбменДанными.Загрузка = Истина;
        ДокументОбъект.Записать(); // Здесь возникает ошибка "В данной транзакции уже происходили ошибки!"
    КонецПопытки;
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
КонецПопытки;

При выполнении этого кода, если при проведении документа (ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение)) возникает какая-либо ошибка базы данных, то при попытке повторной записи документа (ДокументОбъект.Записать()) внутри внутреннего блока Исключение, мы получаем ошибку "В данной транзакции уже происходили ошибки!". При этом, если убрать внутренний блок Попытка ... Исключение и просто записать документ без проведения, ошибка исчезает. Почему так происходит?

Почему возникает ошибка "В данной транзакции уже происходили ошибки!"?

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

  1. Состояние транзакции после ошибки базы данных. Когда внутри активной транзакции происходит любая ошибка, связанная с базой данных (например, нарушение уникальности, конфликт блокировок, ошибка проведения), платформа 1С на уровне СУБД помечает эту транзакцию как "испорченную" или "только для отмены". Это критически важный момент: после такой пометки любые дальнейшие операции записи или чтения данных в рамках этой транзакции становятся невозможными, за исключением её полной отмены.

  2. Подавление ошибок и продолжение выполнения. В нашем примере, внутренний блок Попытка ... Исключение предназначен для обработки возможных ошибок проведения. Если при выполнении ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение) возникает ошибка базы данных, управление переходит в блок Исключение. Если в этом блоке мы не вызываем ВызватьИсключение, то код продолжает выполняться. В этот момент внешняя транзакция (начатая с НачатьТранзакцию()) все еще активна, но уже помечена как "испорченная" из-за первой ошибки.

  3. Повторное обращение к базе данных в "испорченной" транзакции. Следующая строка во внутреннем блоке ИсключениеДокументОбъект.Записать() – представляет собой повторное обращение к базе данных. Поскольку транзакция уже "испорчена", платформа 1С не позволяет выполнить эту операцию, и мы получаем ошибку "В данной транзакции уже происходили ошибки!". Важно отметить, что сообщение об ошибке указывает на место повторного обращения к БД, а не на место первоначальной ошибки, что может затруднить отладку.

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

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

Рекомендации по правильной работе с транзакциями и проведением документов

Учитывая особенности работы транзакций в 1С, рассмотрим несколько подходов к решению данной проблемы.

1. Корректная обработка ошибок внутри транзакции

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

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

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

Вот как мог бы выглядеть исправленный код, если мы хотим, чтобы при ошибке проведения вся транзакция была отменена, а информация об ошибке передана дальше:


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

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

2. Разделение записи и проведения документа

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

Рассмотрим по шагам, как это реализовать:

  1. Запись документа в транзакции: Сначала мы создаем и записываем документ в транзакции, но без проведения (РежимЗаписиДокумента.Запись). Это гарантирует, что сам документ будет создан в базе данных, а любые ошибки на этом этапе приведут к отмене всей транзакции.

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

Пример кода:


СсылкаНовогоДокумента = Неопределено;

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

// Если документ успешно записан, пытаемся его провести вне транзакции
Если СсылкаНовогоДокумента <> Неопределено Тогда
    Попытка
        ДокументОбъектДляПроведения = СсылкаНовогоДокумента.ПолучитьОбъект();
        Если ДокументОбъектДляПроведения <> Неопределено Тогда
            ДокументОбъектДляПроведения.Записать(РежимЗаписиДокумента.Проведение);
            Сообщить("Документ " + СсылкаНовогоДокумента + " успешно проведен.");
        КонецЕсли;
    Исключение
        // Здесь обрабатываем ошибки проведения.
        // Транзакция записи документа уже зафиксирована.
        Сообщить("Внимание! Документ " + СсылкаНовогоДокумента + " записан, но не проведен. Ошибка: " + ОписаниеОшибки());
        // Документ останется непроведенным, можно пометить его для отложенного проведения
    КонецПопытки;
КонецЕсли;

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

3. Отложенное проведение с помощью регламентных заданий

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

Идея заключается в следующем:

  1. Запись документа в транзакции: Как и в предыдущем методе, документ создается и записывается (без проведения) в транзакции. После успешной записи транзакция фиксируется.

  2. Регистрация документа для отложенного проведения: После записи документа, мы записываем информацию о нем (например, его ссылку) в специальный регистр сведений. Назовем его, например, ВыполнениеОтложенныхДействий.

  3. Обработка регламентным заданием: Отдельное регламентное задание периодически (например, раз в минуту или час) просматривает этот регистр сведений. Для каждого документа в списке оно пытается выполнить проведение. Каждое проведение выполняется в своей собственной Попытка ... Исключение, что позволяет обработать ошибки индивидуально, не влияя на другие документы.

  4. Обновление статуса: После успешного проведения или обработки ошибки, запись в регистре сведений обновляется (например, меняется статус на "Выполнено" или "Ошибка проведения") или удаляется.

Рассмотрим структуру регистра сведений ВыполнениеОтложенныхДействий:

Пример кода записи в регистр (после успешной записи документа в транзакции):


// Предполагаем, что СсылкаНовогоДокумента получена после успешной записи
Если СсылкаНовогоДокумента <> Неопределено Тогда
    Движение = РегистрыСведений.ВыполнениеОтложенныхДействий.СоздатьМенеджерЗаписи();
    Движение.Объект = СсылкаНовогоДокумента;
    Движение.ТекстАлгоритма = "ПровестиДокумент";
    Движение.Состояние = Перечисления.СостоянияОтложенныхДействий.КВыполнению;
    Движение.Записать();
КонецЕсли;

Пример логики регламентного задания:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ВыполнениеОтложенныхДействий.Объект КАК Объект,
    |    ВыполнениеОтложенныхДействий.ТекстАлгоритма КАК ТекстАлгоритма
    |ИЗ
    |    РегистрСведений.ВыполнениеОтложенныхДействий КАК ВыполнениеОтложенныхДействий
    |ГДЕ
    |    ВыполнениеОтложенныхДействий.Состояние = &СостояниеКВыполнению";

Запрос.УстановитьПараметр("СостояниеКВыполнению", Перечисления.СостоянияОтложенныхДействий.КВыполнению);

Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();

Пока Выборка.Следующий() Цикл
    МенеджерЗаписи = РегистрыСведений.ВыполнениеОтложенныхДействий.СоздатьМенеджерЗаписи();
    МенеджерЗаписи.Объект = Выборка.Объект;
    МенеджерЗаписи.Прочитать(); // Прочитаем текущее состояние

    Попытка
        ДокументОбъект = Выборка.Объект.ПолучитьОбъект();
        Если ДокументОбъект <> Неопределено Тогда
            Если Выборка.ТекстАлгоритма = "ПровестиДокумент" Тогда
                ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
                МенеджерЗаписи.Состояние = Перечисления.СостоянияОтложенныхДействий.Выполнено;
                МенеджерЗаписи.ОписаниеОшибки = "";
                Сообщить("Документ " + Выборка.Объект + " успешно проведен регламентным заданием.");
            КонецЕсли;
        КонецЕсли;
    Исключение
        МенеджерЗаписи.Состояние = Перечисления.СостоянияОтложенныхДействий.Ошибка;
        МенеджерЗаписи.ОписаниеОшибки = ОписаниеОшибки();
        Сообщить("Ошибка проведения документа " + Выборка.Объект + " регламентным заданием: " + ОписаниеОшибки());
    КонецПопытки;
    
    МенеджерЗаписи.Записать();
КонецЦикла;

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

Заключение

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

Мы рассмотрели три основных подхода:

  1. Корректная обработка ошибок внутри транзакции: Отмена всей транзакции и вызов исключения при любой ошибке базы данных.
  2. Разделение записи и проведения: Запись документа в транзакции, а его проведение — вне транзакции, с отдельной обработкой ошибок.
  3. Отложенное проведение с помощью регламентных заданий: Запись документа, регистрация его для проведения в регистре сведений и последующая асинхронная обработка регламентным заданием.

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

← К списку