Безопасная работа с транзакциями во встроенном языке
Разбираемся с опасностями использования транзакций во встроенном языке 1С. Познаем ошибку «В данной транзакции уже происходили ошибки». Учимся защищаться от них.
- Описание
- Подробнее
Описание
Статья актуальна для версии платформы 8.3.14
В этой статье я постарался систематизировать статьи ИТС и собственный опыт в плане обхода опасностей, сопровождающих использование транзакций в 1С. Транзакционные блокировки в статье не рассматриваются. К статье приложена демо база. При ее запуске откроется обычное приложение и форма с кнопками для выполнения примеров, на которые ссылается статья серым шрифтом.
Транзакции применяются для целостного изменения связанных данных, т.е. все действия с базой данных, выполняемые в рамках транзакции или выполняются целиком, или целиком откатываются.
- Менеджер транзакции
- Это условное название единого для сеанса базы 1С внутреннего объекта платформы, управляющего транзакцией. Единый для сеанса значит, что он синхронизируется между толстым клиентским и серверным контекстном сеанса.
- Свойства менеджера транзакции
- Глубина/Вложенность/Счетчик — целое число — сколько раз была открыта транзакция минус сколько раз была закрыта
- Отменена – булево — признак отмены транзакции
- Вложенные транзакции
- Менеджер транзакции содержит свойство “Глубина”. Если оно больше 1, то транзакция считается вложенной.
- Логические (1С) и фактические (СУБД) транзакции
- Логическая (1С) транзакция — все операции между началом транзакции и следующим завершением транзакции с тем же значением глубины/вложенности/счетчика транзакций
- Фактическая (СУБД) транзакция — совпадает с логической транзакцией с Глубина = 1
- Вложенными в 1С могут быть только логические транзакции
- При начале логической транзакции увеличивается на 1 свойство “Глубина” менеджера транзакции
- При завершении логической транзакции уменьшается на 1 свойство “Глубина” менеджера транзакции
- Определить во встроенном языке значение свойства Глубина или хотя бы наличие одной вложенности, не изменяя состояние менеджера транзакции, невозможно.
- Вложенность транзакций (ИТС)
- Правила использования транзакций (ИТС)
- Явные и неявные логические транзакции
- Явные — начинаются/завершаются методами встроенного языка
- НачатьТранзакцию
- ЗафиксироватьТранзакцию/ОтменитьТранзакцию
- Неявные — начинаются/завершаются платформой в начале/конце выполнения записи объектов данных. При этом программная запись влияет на свойство Глубина, а интерактивная нет.
- Определить во встроенном языке, является ли транзакция явной/неявной, невозможно.
- Явные — начинаются/завершаются методами встроенного языка
- Сломанные транзакции
- Менеджер транзакции содержит признак “Отменена”. Сбрасывается он только при начале фактической транзакции. Если он установлен, то транзакция считается сломанной и фактическая транзакция подлежит отмене при ее любом завершении. Устанавливается он при возникновении ошибки базы данных и при вызове ОтменитьТранзакцию().
- Определить во встроенном языке, является ли транзакция сломанной, напрямую невозможно, но можно косвенно с достаточной долей уверенности. Пример будет рассмотрен далее.
- Примеры ошибок базы данных
- Ошибка выполнения запроса
- Необработанное исключение при записи объекта
- Отказ при записи объекта (“Не удалось записать <объект>”)
- Ошибка установки транзакционной блокировки
- Ошибка установки объектной блокировки
- Ошибки базы данных и транзакции (ИТС)
- Невосстановимые и восстановимые исключения (ИТС)
- При обращении к БД в сломанной транзакции платформа выбрасывает ошибку “В данной транзакции уже происходили ошибки”. Здесь возникает разрыв между первичной ошибкой, ломающей транзакцию, и этой вторичной ошибкой. Из-за этого разрыва разработчику обычно бывает очень тяжело добраться до причины первичной ошибки. Поэтому к обработке ошибок в сломанной транзакции нужно подходить очень аккуратно. Пример будет рассмотрен далее.
- Работа со ссылками в фактической (СУБД) транзакции
- Кэш представлений ссылок
- Транзакция имеет собственный кэш представлений ссылок.
- Обращение к представлению ссылки может вызывать неявное обращение к БД для обновления кэша представления по этой ссылке.
- Примеры обращений к представлению ссылки
- “” + Ссылка
- Таблица.Сортировать(“Ссылка”) — платформа считывает представления ссылок для сортировки по ним
- ЗаписьЖурналаРегистрации(,,, Ссылка) — всегда получает представление от ссылки
- При обработке исключений в транзакции часто возникает потребность вывода диагностической информации в лог/пользователю. Тут кроется самая коварная особенность сломанной транзакции. При получении представления ссылки с обновлением кэша в сломанной транзакции платформа выбрасывает необрабатываемое исключение без указания исходной строки, в которой выполнено это обращение. Причем если код выполняется внутри неявной транзакции, то исключение является восстановимым, а иначе не восстановимым. Об этом очень неудобном поведении я сообщал в 1С в 2013г и в 2019г, но невосстановимость этой ошибки до сих пор осталась. Далее будет приведен безопасный подход к решению задачи.
- Объектный кэш
- Транзакция имеет собственный объектный кэш.
- Обращение к полю ссылки может вызывать неявное обращение к БД для обновления кэша объекта по этой ссылке.
- Примеры обращений к объектному кэшу
- Ссылка.ПометкаУдаления;
- Ссылка.ПолучитьОбъект();
- Аналогично кэшу представлений ссылок при обращении к объектному кэшу в сломанной транзакции платформа может выбрасывать необрабатываемые и невосстановимые исключения, но в меньшем числе ситуаций.
- Кэш представлений ссылок
- Взаимоблокировка (Deadlock)
- Чтобы снизить вероятность появления взаимоблокировок, нужно стараться устанавливать управляемые блокировки, нужные для всех вложенных транзакций, в самом начале фактической (СУБД) транзакции. Тогда выполнение кода установки управляемых блокировок во вложенных транзакциях не будет изменять состав заблокированных ресурсов и тем самым порядок захвата ресурсов в транзакции будет более стабильным и предсказуемым.
- Чтобы снизить вероятность появления взаимоблокировок, нужно стараться устанавливать управляемые блокировки, нужные для всех вложенных транзакций, в самом начале фактической (СУБД) транзакции. Тогда выполнение кода установки управляемых блокировок во вложенных транзакциях не будет изменять состав заблокированных ресурсов и тем самым порядок захвата ресурсов в транзакции будет более стабильным и предсказуемым.
Таблица операций, воздействующих на менеджер транзакции
Операция | Выброс исключения | Признак отмены транзакции | Глубина | Фактическая транзакция (СУБД) |
Начало записи объекта (неявное начало транзакции) | Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки” | Ложь, если глубина была 0 | +1 при программной записи; не меняется при интерактивной записи | Открывается, если глубина была 0 |
Конец записи объекта (неявный конец транзакции) |
| Истина, если Отказ = Истина или выброшено исключение | -1 при программной записи; не меняется при интерактивной записи | Если глубина стала 0
|
НачатьТранзакцию | Ложь, если глубина была 0 | +1 | Открывается, если глубина была 0 | |
ОтменитьТранзакцию | Если глубина была 0, то “Транзакция неактивна” | Истина | -1 | Откатывается, если глубина стала 0. |
ЗафиксироватьТранзакцию | Если глубина была 0, то “Транзакция неактивна” | не меняется | -1 | Если глубина стала 0
|
Операция (явная или неявная) с БД в транзакции | Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки”
| Истина, если выброшено исключение | не меняется | не меняется |
| Завершение потока встроенного языка | 0 | Если глубина была > 0, Откатывается |
Исключение в транзакции
Ошибки в транзакции могут быть
- Ломающие — связаны с БД, выставляют признак "Отменена" менеджера транзакции
- Неломающие — не связаны с БД, не воздействуют на менеджер транзакции
Проверка сломанной транзакции
Хотя явно получить значение признака “Отменена” менеджера транзакции во встроенном языке нельзя. Однако можно воспользоваться косвенным методом — попытаться выполнить простейшую операцию БД (общий модуль ОбщийМодуль1)
// Будет ли выброшено исключение "В данной транзакции уже происходили ошибки" (является ли транзакция сломанной)
Функция ВТранзакцииПроисходилиОшибки() Экспорт
Запрос = Новый Запрос("ВЫБРАТЬ 1");
Попытка
Запрос.Выполнить();
Результат = Ложь;
Исключение
Результат = Истина;
КонецПопытки;
Возврат Результат;
КонецФункции
При необходимости устранить ложные срабатывания, можно будет еще анализировать описание ошибки. Кстати обращения к серверу СУБД этот запрос не производит (только к модели БД).
Подготовка данных для обработки исключения в сломанной транзакции
В ряде случаев имеет смысл готовить данные для обработки исключения заранее, т.е. до выполнения опасной операции с БД, которая может сломать транзакцию. Особенно актуально это для предотвращения обращений к объектному кэшу и кэшу представлений, которые могут вызывать невосстановимые ошибки в сломанной транзакции в случае необходимости считывания данных в этот кэш. Также в рамках этой подготовки можно наполнить объектный кэш и кэш представлений ссылок нужными ссылками, но делать это нужно в транзакции. Тут важно найти оптимальный баланс между дополнительными подготовительными обращениями к БД и диагностической ценностью этих данных для обработки потенциальных исключений. В случае риска обращений к кэшу представлений для регистрации диагностической информации вместо подготовки данных можно в качестве альтернативы использовать идентификаторы ссылок.
Передача ссылки в журнал регистрации в сломанной транзакции
При обработке исключения в сломанной транзакции часто разумно писать диагностическую информацию в журнал регистрации. При этом метод ЗаписьЖурналаРегистрации() неявно берет представление от ссылки, используя кэш представлений ссылок, и помещает его в поле "Представление данных" события журнала. Обращение к этому кэшу в сломанной транзакции несет риск невосстановимой ошибки. Поэтому рекомендую передавать ссылку в журнал регистрации только через эту функцию (общий модуль ОбщийМодуль1)
// Помещение представления ссылки в кэш представлений в сломанной транзакции вызывает досрочное завершение фактической транзакции с невосстановимой ошибкой "В данной транзакции уже происходили ошибки"
// Передача в метод ЗаписьЖурналаРегистрации() ссылки на объект в сломанной транзакции приведет к такому исключению, если представление будет обновлено в кэше.
// Поэтому в таком случае функция возвращает строку идентификатор ссылки. Подразумевается что такие места в коде будут устраняться и потому возвращаться идентификатор будет достаточно редко.
Функция СсылкаДляПередачиВЖурналРегистрации(Ссылка) Экспорт
Если ВТранзакцииПроисходилиОшибки() Тогда
Результат = "" + Ссылка.УникальныйИдентификатор();
Иначе
Результат = Ссылка;
КонецЕсли;
Возврат Результат;
КонецФункции
Тогда безопасная запись в журнал регистрации при обработке исключения в транзакции может выглядеть так
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), СсылкаДляПередачиВЖурналРегистрации(Ссылка));
...
КонецПопытки;
Открывать и закрывать транзакцию в одном методе
Старайтесь открывать и закрывать логическую транзакцию в одном методе. Это значительно облегчает отладку и анализ кода. Следствие этой рекомендации — выполнять код транзакции в попытке, чтобы в исключении можно было отменить транзакцию. Иначе исключение поднимется по стеку (в вызывающий метод) без закрытия логической транзакции (восстановления свойства Глубина менеджера транзакции) в текущем методе. Часто и особенно во вложенных транзакциях при обработке такого исключения и после возможной регистрации диагностической информации исключение перевыбрасывают. Даже если на текущий момент ваша транзакция не вызывается вложенно потом это может измениться. Поэтому разумно сразу об этом позаботиться.
Обработка исключения при записи в попытке
Одним из частых вариантов превращения транзакции в сломанную является запись объекта в БД в попытке без транзакции. Допустим есть корректно работающий метод, выполняющий запись документа с регистраций в журнал регистрации ошибки, если такая возникает, но без перевыброса исключения. Если этот метод будет вызван внутри выше стоящей транзакции, то при попытке ее фиксации будет выброшено исключение "В данной транзакции уже происходили ошибки". Поэтому при заворачивании операции с БД в попытку рекомендую в обработке исключения в конце вставлять перевыброс исключения в случае наличия внешней транзакции.
Попытка
Объект.Записать();
Исключение
ЗаписьЖурналаРегистрации("ОшибкаЗаписи", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
Если ТранзакцияАктивна() Тогда
ВызватьИсключение;
КонецЕсли;
Результат = Ложь;
КонецПопытки;Операции с БД в сломанной транзакции
Иногда бывает нужно в сломанной транзакции (например при обработке исключения) выполнить очень важную операцию с БД.
В таком случае может быть оправдано пренебречь рекомендацией "Открывать и закрывать транзакцию в одном методе" и завершить фактическую транзакцию в текущем месте, чтобы сделать возможным выполнение этой важной операции с БД.
Исключение
Пока ТранзакцияАктивна() Цикл
ОтменитьТранзакцию();
КонецЦикла;
ЗаписатьИнформациюОбОшибкеВРегистр();
...
КонецПопытки;
Следует иметь ввиду, что такой прием повышает вероятность возникновения ошибок во внешних транзакциях. Самый негативный сценарий — текущая транзакция была вложена в неявную фактическую транзакцию записи объекта. Тогда при завершении записи объекта будет невосстановимое исключение (кнопка "Невосстановимая ошибка после отмены транзакции в неявной транзакции").
Также для выполнения очень важной операции с БД в сломанной транзакции можно использовать запуск фонового задания.
Рекомендуемая структура кода транзакции
- метод НачатьТранзакцию рекомендуется располагать за пределами блока Попытка-Исключение непосредственно перед оператором Попытка;
