Как работают транзакции и блок Попытка-Исключение в 1С: развенчиваем мифы и строим надежный код

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

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

В чем принципиальная разница между Попыткой и Транзакцией?

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

Транзакция — это неделимая группа операций с базой данных, которая переводит БД из одного согласованного состояния в другое. Транзакции подчиняются строгим правилам ACID (атомарность, согласованность, изолированность, долговечность). Главная задача транзакции — гарантировать, что либо все изменения данных внутри нее будут сохранены (зафиксированы), либо, в случае сбоя, база данных вернется в исходное состояние (произойдет откат).

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

Эксперимент: доказываем разницу на практике

Проанализируем классический пример, который наглядно демонстрирует поведение системы в обоих случаях. Представим, что нам необходимо создать два новых элемента в справочнике Номенклатура.

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


Попытка
    Спр1 = Справочники.Номенклатура.СоздатьЭлемент();
    Спр1.Наименование = "Тестовый товар 1";
    Спр1.Записать(); // Элемент успешно записывается в базу данных
    
    Спр2 = Справочники.Номенклатура.СоздатьЭлемент();
    Спр2.Наименование = "Тестовый товар 2";
    Спр2.Записать(); // Элемент успешно записывается в базу данных
    
    // Искусственно вызываем ошибку деления на ноль
    ОшибкаДеления = 1 / 0; 
Исключение
    // Сюда мы попадем после деления на ноль. 
    // Ошибка перехвачена, выполнение кода продолжается.
КонецПопытки;

Что произойдет в базе данных после выполнения этого фрагмента кода? Программа не завершится аварийно, управление перейдет в блок Исключение, а затем код пойдет дальше. Но если мы зайдем в справочник номенклатуры, мы увидим оба созданных элемента: "Тестовый товар 1" и "Тестовый товар 2". Блок Попытка подавил ошибку, но никак не отменил запись объектов.

Теперь рассмотрим второй вариант, где мы явно открываем транзакцию:


НачатьТранзакцию();
Попытка
    Спр3 = Справочники.Номенклатура.СоздатьЭлемент();
    Спр3.Наименование = "Тестовый товар 3";
    Спр3.Записать();
    
    Спр4 = Справочники.Номенклатура.СоздатьЭлемент();
    Спр4.Наименование = "Тестовый товар 4";
    Спр4.Записать();
    
    ОшибкаДеления = 1 / 0; // Снова провоцируем ошибку
    
    // Если бы ошибки не было, мы бы зафиксировали изменения
    ЗафиксироватьТранзакцию();
Исключение
    // В случае любой ошибки мы обязаны явно откатить транзакцию
    ОтменитьТранзакцию();
КонецПопытки;

В этом случае, как только выполнение дойдет до деления на ноль, система перейдет в блок Исключение, где будет выполнен метод ОтменитьТранзакцию(). База данных мгновенно откатит все изменения, сделанные с момента вызова НачатьТранзакцию(). В результате в справочнике номенклатуры не появится ни "Тестовый товар 3", ни "Тестовый товар 4". Целостность базы данных сохранена.

Что такое «битая» транзакция (состояние setRollbackOnly)?

Часто разработчики сталкиваются со странной и пугающей ошибкой: «В данной транзакции уже происходили ошибки!». Давайте разберем природу этого явления подробнее.

Когда внутри активной транзакции происходит любая критическая ошибка СУБД или платформы (например, конфликт блокировок, нарушение уникальности индексов, отказ в записи из-за проверок в подписках на события), СУБД переводит текущую транзакцию в специальный защитный режим — «только для отмены» (в терминологии баз данных этот флаг называется setRollbackOnly).

Даже если мы обернули проблемный участок кода в блок Попытка — Исключение и успешно перехватили ошибку встроенным языком, транзакция внутри СУБД уже считается «мертвой». Мы больше не можем выполнять внутри нее операции записи или чтения. Любая последующая попытка обратиться к базе данных (даже простое получение реквизита или чтение константы) будет приводить к генерации ошибки «В данной транзакции уже происходили ошибки!».

Единственный правильный выход при переводе транзакции в это состояние — немедленно выполнить метод ОтменитьТранзакцию(). Пытаться продолжить работу программы внутри «битой» транзакции бессмысленно и вредно.

Реальная механика «вложенных» транзакций в 1С

Платформа «1С:Предприятие 8» позволяет разработчикам многократно вызывать метод НачатьТранзакцию() внутри одной сессии. Может показаться, что система поддерживает полноценные вложенные транзакции, но это иллюзия.

На уровне современных СУБД (MS SQL Server, PostgreSQL, Oracle) реальных вложенных транзакций не существует. Платформа 1С имитирует их с помощью счетчика глубины транзакций:

  1. Первый вызов НачатьТранзакцию() открывает реальную физическую транзакцию на уровне СУБД и устанавливает счетчик в значение 1.
  2. Каждый последующий вызов НачатьТранзакцию() лишь увеличивает внутренний счетчик платформы на единицу (логическая транзакция).
  3. Вызов метода ЗафиксироватьТранзакцию() уменьшает счетчик на единицу. Физический коммит (сохранение изменений в БД) происходит только тогда, когда счетчик транзакций становится равным 0.
  4. Если на любом уровне вложенности вызывается метод ОтменитьТранзакцию(), вся транзакция СУБД откатывается мгновенно и целиком, независимо от текущего значения счетчика. Логическая цепочка разрушается, и последующие попытки внешних процедур вызвать ЗафиксироватьТранзакцию() завершатся критической ошибкой.

Неявные транзакции платформы: о чем нельзя забывать

Нам не обязательно всегда прописывать НачатьТранзакцию() вручную. Платформа 1С очень часто открывает транзакции автоматически. Это называется неявными транзакциями. Они запускаются при вызове методов:

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

Единый стандарт безопасной работы с транзакциями

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


// Шаг 1: Начинаем транзакцию строго ВНЕ блока Попытка
НачатьТранзакцию();

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

Проанализируем критически важные правила этого шаблона:

  1. Вызов НачатьТранзакцию() должен находиться вне блока Попытка. Если поместить его внутрь блока Попытка, и ошибка возникнет до инициализации транзакции (например, при подготовке данных), управление перейдет в блок Исключение. Там система попытается выполнить метод ОтменитьТранзакцию(), что вызовет новую ошибку («Транзакция не активна»), которая полностью затрет исходную причину сбоя в стеке вызовов.
  2. Фиксация транзакции должна быть в блоке Попытка. Мы должны пытаться зафиксировать изменения только тогда, когда весь предшествующий код выполнился абсолютно успешно.
  3. Первой строкой в Исключении должен быть откат. Метод ОтменитьТранзакцию() должен отрабатывать незамедлительно при возникновении сбоя, чтобы освободить заблокированные ресурсы СУБД.
  4. Нельзя «гасить» ошибки внутри транзакции. Оставлять блок Исключение пустым внутри транзакции — грубейшее нарушение стандартов разработки. Если база данных вернула ошибку, транзакция уже переведена в режим отката. Попытка скрыть ошибку приведет к непредсказуемому поведению системы и логическому повреждению данных.

Транзакции и внешние системы: классическая ловушка

Рассмотрим очень важную архитектурную проблему, с которой часто сталкиваются разработчики при интеграции 1С с торговым оборудованием (кассы, терминалы), отправкой SMS или внешними API (веб-сервисы, REST-интеграции).

СУБД гарантирует транзакционность только для своих внутренних таблиц. Внешний мир (веб-сервис стороннего банка, фискальный регистратор на кассе, шлюз рассылки сообщений) ничего не знает о транзакции 1С и не умеет откатывать свои действия назад. Механизма двухфазного коммита (2PC) в стандартных механизмах интеграции 1С нет.

Представим неправильный сценарий работы кассового узла:


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

Что произошло в результате этого сбоя? В базе данных 1С транзакция успешно откатилась: документ чека аннулирован или не записан. Но фискальный регистратор уже физически напечатал бумажный чек и отправил данные в ОФД, а покупатель оплатил товар картой на терминале. Мы получили критическую рассинхронизацию между учетом в 1С и реальными действиями оборудования.

Золотое правило архитектуры 1С: внутри транзакций запрещено выполнять любые необратимые действия во внешних системах. Сначала мы должны полностью завершить, проверить и зафиксировать транзакцию внутри базы данных 1С, и только после успешного сохранения данных отправлять команды на кассовое оборудование, отправлять SMS-уведомления клиентам или обращаться к сторонним веб-сервисам.

← На главную