При разработке сложных алгоритмов записи данных в 1С мы часто используем механизм транзакций для обеспечения целостности информации. Однако у начинающих (и иногда опытных) разработчиков возникает распространенное заблуждение: кажется, что команда отмены транзакции действует как "машина времени", возвращая состояние всей программы в момент до начала транзакции. В этой статье мы подробно разберем, почему изменения интерфейса (например, видимость кнопок) остаются, даже если произошла ошибка, и как правильно проектировать такой код.
Рассмотрим ситуацию, с которой столкнулся автор вопроса. Логика кода строится на предположении, что блок транзакции защищает абсолютно все действия внутри него.
Взглянем на пример кода, который вызывает недоумение:
НачатьТранзакцию();
Попытка
// Меняем интерфейс
Элементы.МойРеквизит.Видимость = Истина;
// Искусственно вызываем ошибку
а = 1/0;
ЗафиксироватьТранзакцию();
Исключение
// Откатываем транзакцию
ОтменитьТранзакцию();
КонецПопытки;
Логика разработчика такова: "Раз произошла ошибка (деление на ноль), мы попадаем в Исключение, выполняется ОтменитьТранзакцию(), и, следовательно, видимость реквизита должна вернуться в значение Ложь". Но на практике реквизит остается видимым. Давайте разберемся, почему так происходит.
Чтобы понять поведение системы, нам нужно четко разделить два понятия: База данных и Оперативная память приложения.
НачатьТранзакцию, ЗафиксироватьТранзакцию и ОтменитьТранзакцию — это команды, передаваемые серверу СУБД (SQL Server, PostgreSQL и т.д.). Они гарантируют, что набор записей в таблицы либо применится целиком, либо не применится вовсе.Видимость, Доступность, Заголовок), значения локальных переменных модуля, состояние открытых окон — все это живет в оперативной памяти клиентского приложения или сервера 1С. Менеджер транзакций базы данных не имеет доступа к этой области памяти и не отслеживает ее состояние.Когда мы пишем Элементы.МойРеквизит.Видимость = Истина, это действие выполняется в памяти моментально. Система не запоминает "старое" состояние для целей транзакции. Поэтому, когда вызывается ОтменитьТранзакцию(), СУБД отменяет изменения в таблицах, но память программы остается в том состоянии, в которое мы ее привели своими действиями.
Что НЕ откатывается при отмене транзакции:
ДокументОбъект), находящихся в памяти, если они не были перечитаны из базы.Поскольку платформа не может автоматически "откатить" интерфейс, мы должны взять эту задачу на себя. Самый надежный способ — менять интерфейс только после того, как мы убедились в успешном завершении транзакции.
Давайте перепишем код, используя переменную-флаг (например, Успех):
Успех = Ложь;
НачатьТранзакцию();
Попытка
// Выполняем опасные действия с данными
// ... запись справочников, документов ...
// Если дошли до этой строки, значит ошибок не было
ЗафиксироватьТранзакцию();
Успех = Истина;
Исключение
ОтменитьТранзакцию();
// Обработка ошибки (запись в журнал, сообщение пользователю)
Успех = Ложь;
КонецПопытки;
// Меняем интерфейс только если транзакция прошла успешно
Если Успех Тогда
Элементы.МойРеквизит.Видимость = Истина;
КонецЕсли;
Такой подход гарантирует, что пользователь увидит изменения в интерфейсе только тогда, когда данные действительно сохранены в базе.
В ходе обсуждения темы был затронут важный вопрос: где правильно размещать вызов НачатьТранзакцию() — внутри блока Попытка или перед ним? Давайте обратимся к стандартам разработки 1С.
Рекомендуемый шаблон выглядит так:
НачатьТранзакцию();
Попытка
// Чтение или запись данных
ДокументОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Дополнительные действия по обработке исключения
ВызватьИсключение; // Или запись в лог
КонецПопытки;
Почему НачатьТранзакцию() пишут ДО попытки?
Это делается для корректной обработки ошибок самого старта транзакции. Если по какой-то причине (например, потеря связи с SQL-сервером) транзакцию начать невозможно, то код упадет с ошибкой еще до входа в блок защиты. Если бы мы поместили старт внутрь, то при ошибке старта мы бы попали в Исключение, где стоит ОтменитьТранзакцию(). Но так как транзакция не началась, попытка её отменить вызвала бы вторую ошибку ("Нет активной транзакции"), которая могла бы запутать диагностику.
Для повышения надежности кода, особенно в сложных процедурах, рекомендуется проверять активность транзакции перед её отменой. Это защищает от случаев, когда транзакция была сброшена системой автоматически или вообще не начиналась.
Модифицируем блок Исключение:
Исключение
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
// Вывод информации об ошибке
Сообщить(ОписаниеОшибки());
КонецПопытки;
Метод ТранзакцияАктивна() возвращает Истина, если в текущем сеансе открыта транзакция, которую можно отменить или зафиксировать.
В 1С существует понятие вложенных транзакций, но работают они специфично. Давайте разберем этот механизм, чтобы избежать ошибок в будущем.
НачатьТранзакцию(). Все последующие вызовы внутри (вложенные) просто увеличивают внутренний счетчик транзакций (+1).ЗафиксироватьТранзакцию() уменьшает счетчик (-1). Данные реально записываются в БД (COMMIT) только когда счетчик достигает нуля.ОтменитьТранзакцию() работает иначе. Она мгновенно отменяет всю транзакцию целиком, независимо от уровня вложенности, и сбрасывает счетчик в ноль.Это означает, что вы не можете отменить только "внутреннюю" часть действий, сохранив "внешнюю". Любая ошибка с отменой транзакции откатывает все изменения в базе данных с самого начала первой транзакции.
Подводя итог, сформулируем основные правила работы с транзакциями для решения задачи автора:
НачатьТранзакцию перед Попытка, ЗафиксироватьТранзакцию в конце блока Попытка, ОтменитьТранзакцию в блоке Исключение.ТранзакцияАктивна() для безопасной отмены.