Транзакции и их подводные камни
Коварный вопрос: «В 1С есть вложенные транзакции?» Правильный ответ — «синтаксически да, а по сути одна», и на этом ловят даже опытных.
Транзакция — это «всё или ничего»: набор изменений в базе, который либо применяется целиком (
ЗафиксироватьТранзакцию), либо отменяется целиком (ОтменитьТранзакцию). В 1С вложенные вызовыНачатьТранзакциюне создают отдельных транзакций — считается только внешняя.
Транзакции проверяют на любом собеседовании уровня мидл, потому что здесь легко написать код, который выглядит правильно, но молча портит данные. Три подводных камня — вложенность, «проглоченное» исключение и неверный откат — разберём по очереди.
Подводный камень 1: вложенные транзакции — это одна транзакция
В 1С можно вызвать НачатьТранзакцию внутри уже открытой транзакции. Платформа лишь увеличивает внутренний счётчик вложенности. Реально в базе фиксация происходит, только когда счётчик обнуляется — то есть по внешней ЗафиксироватьТранзакцию.
НачатьТранзакцию(); // счётчик = 1, транзакция реально стартовала
НачатьТранзакцию(); // счётчик = 2, НОВОЙ транзакции НЕ появилось
// ... какие-то записи ...
ЗафиксироватьТранзакцию(); // счётчик = 1, в базу ЕЩЁ НИЧЕГО не записано
ЗафиксироватьТранзакцию(); // счётчик = 0, вот теперь всё пишется в базу
Практическое следствие: если где-то в глубине вызовется ОтменитьТранзакцию, откатится вся внешняя транзакция целиком, а не только «внутренняя». Никакого частичного отката до «точки сохранения» в 1С нет. Именно поэтому на вопрос «а можно откатить только вложенную часть?» правильный ответ — «нет, откат всегда полный».
Подводный камень 2: нельзя «глотать» исключение внутри транзакции
Самая опасная ошибка. Новичок оборачивает запись в Попытка ... Исключение, ловит ошибку, пишет её в лог и... продолжает работать, будто ничего не случилось. Но транзакция при исключении внутри уже помечена платформой как «испорченная» — в неё больше нельзя писать, её можно только отменить.
// ПЛОХО: исключение «проглочено», транзакция продолжается
НачатьТранзакцию();
Попытка
ОбъектДокумента.Записать();
Исключение
// проглотили ошибку и поехали дальше — так делать НЕЛЬЗЯ
ЗаписьЖурналаРегистрации("Проведение", УровеньЖурналаРегистрации.Ошибка);
КонецПопытки;
ДругойОбъект.Записать(); // пишем в уже испорченную транзакцию
ЗафиксироватьТранзакцию(); // тут прилетит ошибка «в транзакции произошла ошибка»
Что произойдёт: после исключения внутри транзакции любая следующая операция и особенно ЗафиксироватьТранзакцию сами возбудят исключение. В лучшем случае вы получите понятную ошибку, в худшем — запутанный сбой далеко от места настоящей проблемы. А данные останутся в неопределённом состоянии.
Как правильно: поймал — откатил — пробросил
// ХОРОШО: при ошибке откатываем транзакцию и пробрасываем исключение выше
НачатьТранзакцию();
Попытка
ОбъектДокумента.Записать();
ДругойОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
ЗаписьЖурналаРегистрации("Проведение", УровеньЖурналаРегистрации.Ошибка, , , ТекстОшибки);
ВызватьИсключение; // не прячем проблему — пробрасываем наверх
КонецПопытки;
Три обязательных шага в блоке Исключение: 1) ОтменитьТранзакцию — вернуть базу в согласованное состояние; 2) залогировать через ИнформацияОбОшибке(), чтобы понять, что случилось; 3) ВызватьИсключение — пробросить ошибку вызывающему коду, чтобы он тоже узнал о провале, а не считал операцию успешной.
Как это работает под капотом
Правило симметрии: на каждый НачатьТранзакцию должен приходиться ровно один ЗафиксироватьТранзакцию или один ОтменитьТранзакцию. Если завершить метод, не закрыв транзакцию, платформа обнаружит незавершённую транзакцию и завершит её принудительно с ошибкой. Поэтому Попытка...Исключение вокруг тела транзакции — не роскошь, а обязательный каркас: он гарантирует, что при любой ошибке транзакция будет закрыта отменой, а не «зависнет».
И ещё: транзакция и блокировки связаны неразрывно. Все блокировки, взятые внутри, снимаются в момент фиксации или отмены. Значит, чем дольше открыта транзакция, тем дольше висят блокировки и тем выше риск конфликтов и deadlock из прошлого урока. Вывод — держите транзакции короткими и не делайте внутри них лишнего (тем более диалогов с пользователем).
Как отвечать на собеседовании
«Вложенных транзакций в 1С фактически нет: вложенные НачатьТранзакцию только увеличивают счётчик, реальная фиксация — по внешней, а любой откат полный. Исключение внутри транзакции нельзя проглатывать — транзакция становится непригодной для записи. Правильный шаблон: НачатьТранзакцию, затем Попытка с записью и ЗафиксироватьТранзакцию, а в Исключение — ОтменитьТранзакцию, лог через ИнформацияОбОшибке() и ВызватьИсключение. Так гарантируется целостность: либо всё записалось, либо ничего». Это ответ уровня уверенного мидла.
Частые ошибки на собеседовании
- «В 1С полноценные вложенные транзакции». Нет — только счётчик; фиксирует внешняя, откат всегда полный.
- Проглотить исключение. Поймать ошибку внутри транзакции и продолжить писать — прямой путь к «в транзакции произошла ошибка» и порче данных.
- Забыть
ОтменитьТранзакциювИсключение. Тогда транзакция не закрыта корректно, и платформа завершит её принудительно. - Не пробросить ошибку (
ВызватьИсключение). Вызывающий код решит, что всё прошло успешно, хотя запись провалилась. - Держать транзакцию открытой долго. Диалог с пользователем или тяжёлые вычисления внутри транзакции удерживают блокировки и провоцируют конфликты.
Итоги-шпаргалка
- Вложенные
НачатьТранзакцию= один счётчик; фиксирует внешняя, откат всегда полный. - Шаблон записи: НачатьТранзакцию → Попытка(запись + Зафиксировать) → Исключение(Отменить + лог + ВызватьИсключение).
- Исключение внутри транзакции нельзя глотать — после него можно только отменить.
- На каждый старт — ровно один
ЗафиксироватьилиОтменить, симметрично. - Транзакция держит блокировки до завершения — держите её короткой.