Транзакции и их подводные камни

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

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

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

Подводный камень 1: вложенные транзакции — это одна транзакция

В 1С можно вызвать НачатьТранзакцию внутри уже открытой транзакции. Платформа лишь увеличивает внутренний счётчик вложенности. Реально в базе фиксация происходит, только когда счётчик обнуляется — то есть по внешней ЗафиксироватьТранзакцию.

НачатьТранзакцию();                 // счётчик = 1, транзакция реально стартовала
    НачатьТранзакцию();             // счётчик = 2, НОВОЙ транзакции НЕ появилось
        // ... какие-то записи ...
    ЗафиксироватьТранзакцию();      // счётчик = 1, в базу ЕЩЁ НИЧЕГО не записано
ЗафиксироватьТранзакцию();          // счётчик = 0, вот теперь всё пишется в базу

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

Подводный камень 2: нельзя «глотать» исключение внутри транзакции

Самая опасная ошибка. Новичок оборачивает запись в Попытка ... Исключение, ловит ошибку, пишет её в лог и... продолжает работать, будто ничего не случилось. Но транзакция при исключении внутри уже помечена платформой как «испорченная» — в неё больше нельзя писать, её можно только отменить.

// ПЛОХО: исключение «проглочено», транзакция продолжается
НачатьТранзакцию();
Попытка
    ОбъектДокумента.Записать();
Исключение
    // проглотили ошибку и поехали дальше — так делать НЕЛЬЗЯ
    ЗаписьЖурналаРегистрации("Проведение", УровеньЖурналаРегистрации.Ошибка);
КонецПопытки;

ДругойОбъект.Записать();            // пишем в уже испорченную транзакцию
ЗафиксироватьТранзакцию();          // тут прилетит ошибка «в транзакции произошла ошибка»

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

Как правильно: поймал — откатил — пробросил

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

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

Как это работает под капотом

Правило симметрии: на каждый НачатьТранзакцию должен приходиться ровно один ЗафиксироватьТранзакцию или один ОтменитьТранзакцию. Если завершить метод, не закрыв транзакцию, платформа обнаружит незавершённую транзакцию и завершит её принудительно с ошибкой. Поэтому Попытка...Исключение вокруг тела транзакции — не роскошь, а обязательный каркас: он гарантирует, что при любой ошибке транзакция будет закрыта отменой, а не «зависнет».

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

Как отвечать на собеседовании

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

Частые ошибки на собеседовании

  • «В 1С полноценные вложенные транзакции». Нет — только счётчик; фиксирует внешняя, откат всегда полный.
  • Проглотить исключение. Поймать ошибку внутри транзакции и продолжить писать — прямой путь к «в транзакции произошла ошибка» и порче данных.
  • Забыть ОтменитьТранзакцию в Исключение. Тогда транзакция не закрыта корректно, и платформа завершит её принудительно.
  • Не пробросить ошибку (ВызватьИсключение). Вызывающий код решит, что всё прошло успешно, хотя запись провалилась.
  • Держать транзакцию открытой долго. Диалог с пользователем или тяжёлые вычисления внутри транзакции удерживают блокировки и провоцируют конфликты.

Итоги-шпаргалка

  • Вложенные НачатьТранзакцию = один счётчик; фиксирует внешняя, откат всегда полный.
  • Шаблон записи: НачатьТранзакцию → Попытка(запись + Зафиксировать) → Исключение(Отменить + лог + ВызватьИсключение).
  • Исключение внутри транзакции нельзя глотать — после него можно только отменить.
  • На каждый старт — ровно один Зафиксировать или Отменить, симметрично.
  • Транзакция держит блокировки до завершения — держите её короткой.
Проверьте себя
1. Как в 1С работают вложенные вызовы НачатьТранзакцию()?
AКаждый вложенный вызов создаёт полноценную независимую транзакцию, которую можно зафиксировать отдельно
BВложенные вызовы лишь увеличивают внутренний счётчик; реальная фиксация происходит по внешней ЗафиксироватьТранзакцию, а любой откат полный
CВложенный НачатьТранзакцию() вызывает ошибку — 1С запрещает вложенность
DВложенная транзакция автоматически фиксируется сразу после КонецПроцедуры
2. Внутри транзакции произошло исключение при записи. Что нужно сделать в блоке Исключение, чтобы сохранить целостность данных?
AЗаписать ошибку в журнал и продолжить выполнение — транзакция сама восстановится
BВызвать ЗафиксироватьТранзакцию(), чтобы сохранить хотя бы то, что успело записаться
CВызвать ОтменитьТранзакцию(), залогировать ошибку через ИнформацияОбОшибке() и пробросить исключение через ВызватьИсключение
DНичего не делать: если не закрыть транзакцию, платформа сама её зафиксирует