При изучении типовых конфигураций или библиотек, таких как Библиотека Стандартных Подсистем (БСП), разработчики нередко сталкиваются с конструкцией, где выполнение обычного запроса на чтение данных заключено в явную транзакцию:
НачатьТранзакцию();
Попытка
Запрос = Новый Запрос;
// ... заполнение текста запроса и параметров
РезультатыЗапроса = Запрос.ВыполнитьПакет();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
При этом в процедуре полностью отсутствует какая-либо запись объектов или наборов записей. Выясним причину появления таких конструкций, проанализируем поведение платформы и СУБД, а также разберем, когда транзакция при чтении действительно необходима, а когда является ошибкой или атавизмом.
Распространенное заблуждение разработчиков заключается в том, что сам по себе вызов метода НачатьТранзакцию() переводит сеанс в режим с полной изоляцией читаемых данных (например, Snapshot или Repeatable Read), гарантируя, что во время работы транзакции никакие параллельные процессы не изменят считываемые остатки или реквизиты.
Рассмотрим, как работают современные базы данных под управлением платформы 1С:Предприятие 8.3:
Read Committed с механизмом версионирования строк (RCSI / MVCC). Это означает, что транзакция в 1С не блокирует таблицы на чтение и не обеспечивает повторяемость чтения для всей длительности транзакции автоматически.Запрос.Выполнить(), второй запрос вполне может увидеть изменения, сделанные параллельным сеансом между первым и вторым вызовом.Запрос.ВыполнитьПакет() отправляет весь пакет запросов на сторону СУБД единым батчем. Сервер СУБД исполняет его в рамках единой внутренней команды, поэтому для обеспечения целостности между временными таблицами одного пакета открывать внешнюю транзакцию в коде 1С не требуется.Проанализируем практические причины, по которым чтение без записи оборачивается в транзакцию:
БлокировкаДанных может накладывать блокировки только внутри активной транзакции (иначе генерируется исключительная ситуация: Блокировка данных может устанавливаться только в транзакции!). Нередко при рефакторинге модуля саму блокировку убирают или переносят, а открывающую и закрывающую транзакцию конструкции оставляют в коде.БлокировкаДанных в явной транзакции. Если программист открыл транзакцию, но забыл заблокировать пространство данных, цель изоляции не достигается.Использование транзакции там, где она не несет функциональной нагрузки, может вызывать серьезные проблемы производительности и стабильности:
НачатьТранзакцию() сеанс 1С привязывается к конкретному физическому соединению из пула соединений кластера серверов 1С к СУБД и удерживает его до фиксации. Длительные запросы или задержки внутри такой транзакции исчерпывают пул соединений.Попытка ... Исключение без проброса ошибки наружу), платформа помечает транзакцию как поврежденную. Любая попытка вызвать ЗафиксироватьТранзакцию() приведет к аварийному завершению транзакции.Вариант 1. Простое чтение данных (транзакция не требуется)
Если прочитанные данные используются для отображения в отчете, форме или для фонового анализа без изменения базы, удалим транзакционные обертки:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиТоваровОстатки.Номенклатура КАК Номенклатура,
| ОстаткиТоваровОстатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ОстаткиТоваров.Остатки(&Период, ) КАК ОстаткиТоваровОстатки";
Запрос.УстановитьПараметр("Период", ТекущаяДатаСеанса());
РезультатЗапроса = Запрос.Выполнить();
Вариант 2. Ответственное чтение данных с управляемой блокировкой
Если требуется гарантировать, что с момента чтения до момента принятия решения или последующей записи параллельные сеансы не смогут изменить считываемые регистры или объекты, применим явную управляемую блокировку:
НачатьТранзакцию();
Попытка
// Создаем и настраиваем управляемую блокировку
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый; // или Исключительный при подготовке к изменению
ЭлементБлокировки.УстановитьЗначение("Склад", ВыбранныйСклад);
Блокировка.Заблокировать();
// Выполняем чтение заблокированных данных
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиТоваровОстатки.Номенклатура КАК Номенклатура,
| ОстаткиТоваровОстатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ОстаткиТоваров.Остатки(, Склад = &Склад) КАК ОстаткиТоваровОстатки";
Запрос.УстановитьПараметр("Склад", ВыбранныйСклад);
Результат = Запрос.Выполнить();
// Здесь выполняются проверки или запись связанных данных
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Вариант 3. Передача промежуточных данных между несвязанными запросами
Если целью использования транзакции было сохранение временных таблиц между вызовами отдельных запросов, транзакция также не нужна. Для этого предназначен объект МенеджерВременныхТаблиц:
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос1 = Новый Запрос;
Запрос1.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос1.Текст =
"ВЫБРАТЬ
| Ссылка КАК Ссылка
|ПОМЕСТИТЬ ВТ_Документы
|ИЗ
| Документ.РеализацияТоваровУслуг
|ГДЕ
| Проведен = ИСТИНА";
Запрос1.Выполнить();
// Второй запрос использует созданную таблицу без открытия транзакции
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос2.Текст =
"ВЫБРАТЬ
| Ссылка
|ИЗ
| ВТ_Документы";
Результат = Запрос2.Выполнить();
МенеджерВТ.Закрыть();
Таким образом, наличие транзакции вокруг одиночного запроса на чтение без вызова метода Блокировка.Заблокировать() и без записи данных является избыточным кодом. В зависимости от требований к изоляции такой блок следует либо очистить от транзакционных методов, либо дополнить управляемой блокировкой по стандарту ответственного чтения.