Транзакция в 1С — группа изменений базы данных, которая либо записывается целиком, либо целиком откатывается. В коде её начинают методом НачатьТранзакцию(), а завершают одним из двух способов: ЗафиксироватьТранзакцию() сохраняет изменения, ОтменитьТранзакцию() их отменяет. Синтаксис простой, но в примерах времён 8.2 его часто пишут так, что при первой же ошибке получается «В данной транзакции уже происходили ошибки!» или тихий откат, о котором никто не узнаёт. Ниже — шаблон транзакции для 1С 8.3 по стандартам 1С, разбор вложенных транзакций и ошибок, которые из них вырастают.
Платформа сама открывает транзакцию при каждой записи объекта или набора записей. Обработчики ПередЗаписью, ПриЗаписи и ОбработкаПроведения выполняются уже внутри неё, и Отказ = Истина откатывает всё, что успели записать, включая движения. Поэтому оборачивать в НачатьТранзакцию() одиночный Записать() нового объекта незачем — стандарт 1С «Транзакции: правила использования» прямо называет это лишним усложнением.
Явная транзакция нужна в трёх случаях:
БлокировкаДанных, которая держится до конца транзакции;УстановитьИспользованиеИтогов(Ложь)): стандарт требует выполнять отключение, запись и включение итогов в одной транзакции.Методы транзакций доступны в серверном контексте, толстом клиенте и внешнем соединении. Из клиентского кода управляемой формы вызывайте процедуру формы с директивой &НаСервере или экспортную процедуру общего модуля с флагами «Сервер» и «Вызов сервера»; из серверного кода достаточно общего модуля с флагом «Сервер».
Пример — групповое изменение реквизитов нескольких элементов справочника. Элементы должны измениться вместе: если один не записался (не прошла проверка в ПередЗаписью, объект занят другим сеансом), остальные тоже откатываются.
// Изменяет реквизиты нескольких объектов атомарно:
// либо записываются все объекты, либо ни один.
// Контекст: сервер (общий модуль с флагом «Сервер» или &НаСервере в модуле формы).
// ЗначенияРеквизитов - Структура: ключ - имя реквизита, значение - новое значение.
Процедура ИзменитьРеквизитыОбъектов(МассивСсылок, ЗначенияРеквизитов) Экспорт
Если МассивСсылок.Количество() = 0 Тогда
Возврат;
КонецЕсли;
НачатьТранзакцию();
Попытка
// Блокируем объекты до чтения, чтобы между чтением и записью
// их никто не изменил (ответственное чтение).
Блокировка = Новый БлокировкаДанных;
Для Каждого Ссылка Из МассивСсылок Цикл
ЭлементБлокировки = Блокировка.Добавить(Ссылка.Метаданные().ПолноеИмя());
ЭлементБлокировки.УстановитьЗначение("Ссылка", Ссылка);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
КонецЦикла;
Блокировка.Заблокировать();
Для Каждого Ссылка Из МассивСсылок Цикл
Объект = Ссылка.ПолучитьОбъект();
Если Объект = Неопределено Тогда
ВызватьИсключение СтрШаблон(НСтр("ru = 'Не найден объект %1 с идентификатором %2'"),
Ссылка.Метаданные().Представление(), Ссылка.УникальныйИдентификатор());
КонецЕсли;
ЗаполнитьЗначенияСвойств(Объект, ЗначенияРеквизитов);
Объект.Записать();
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// ОбработкаОшибок - платформа 8.3.17 и выше;
// на более старых релизах: ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()).
ЗаписьЖурналаРегистрации(НСтр("ru = 'Групповое изменение реквизитов'"),
УровеньЖурналаРегистрации.Ошибка, , ,
ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
НачатьТранзакцию() стоит непосредственно перед Попытка, а не внутри блока. Если начало транзакции окажется внутри Попытка и ошибка случится раньше него, блок Исключение вызовет ОтменитьТранзакцию() для транзакции, которую никто не начинал. Вне транзакции это даст новую ошибку, которая заслонит исходную, а если снаружи есть чужая транзакция — отменит её уровень.Попытка: блокировка, чтение, запись. Никаких Возврат между началом и фиксацией.Ссылка.Метаданные().ПолноеИмя() вернёт строку вида «Справочник.Номенклатура». Режим Исключительный и так используется по умолчанию, явная строка нужна для читаемости. Если этот объект уже заблокирован управляемой блокировкой в транзакции другого сеанса, Заблокировать() подождёт и по тайм-ауту выбросит исключение — транзакция откатится, ничего не записано. Если же объект открыт на редактирование в форме у другого пользователя (объектная блокировка), ожидания не будет: исключение выбросит Объект.Записать(), и транзакция точно так же откатится целиком.ЗафиксироватьТранзакцию() — последняя строка блока Попытка. После неё не должно быть кода, способного вызвать исключение, иначе обработчик попытается отменить уже зафиксированную транзакцию.Исключение первой строкой — ОтменитьТранзакцию(), затем запись в журнал регистрации, затем ВызватьИсключение без параметров. Последний оператор пробрасывает исходную ошибку выше: вызывающий код узнает, что операция не прошла, а внешняя транзакция, если она есть, тоже будет отменена явно.ОбработкаОшибок.ПодробноеПредставлениеОшибки — вариант из текущей редакции стандарта. В конфигурациях на старых релизах 8.3 используйте глобальную функцию ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()).НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый). Параметр имеет смысл только для конфигураций с режимом блокировок «Автоматический и управляемый»; в конфигурации на управляемом режиме его не пишут.Код транзакции обычно выполняется на сервере. В файловой базе его можно пройти пошагово сразу при запуске из конфигуратора, а в клиент-серверном варианте — только с включённой отладкой на сервере. Тот же шаблон работает без изменений в регламентных заданиях.
Вложенных транзакций в 1С нет. Если НачатьТранзакцию() вызвана, когда транзакция уже идёт, новая не открывается — платформа увеличивает внутренний счётчик. ЗафиксироватьТранзакцию() уменьшает его, а реальная фиксация в СУБД происходит, только когда счётчик доходит до нуля. ОтменитьТранзакцию() на вложенном уровне тоже уменьшает счётчик и вдобавок ставит признак отмены: реального отката в этот момент нет, но зафиксировать транзакцию уже нельзя, и завершение на верхнем уровне закончится откатом всего.
Процедура из примера выше одинаково корректно работает и сама по себе, и внутри внешней транзакции:
// Обе группы должны измениться вместе: или обе, или ни одна.
Процедура ИзменитьДвеГруппы(ПерваяГруппа, ВтораяГруппа, НовыеЗначения) Экспорт
НачатьТранзакцию(); // счётчик 0 -> 1, транзакция в СУБД открыта
Попытка
ИзменитьРеквизитыОбъектов(ПерваяГруппа, НовыеЗначения); // внутри: 1 -> 2 -> 1
ИзменитьРеквизитыОбъектов(ВтораяГруппа, НовыеЗначения); // внутри: 1 -> 2 -> 1
ЗафиксироватьТранзакцию(); // 1 -> 0, реальная фиксация
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
| Вызов | Счётчик | Что происходит в базе |
|---|---|---|
НачатьТранзакцию() снаружи | 1 | транзакция открыта |
НачатьТранзакцию() внутри | 2 | ничего, новой транзакции нет |
ЗафиксироватьТранзакцию() внутри | 1 | ничего, изменения ещё не зафиксированы |
ОтменитьТранзакцию() внутри | 1 | ставится признак отмены, зафиксировать транзакцию больше нельзя |
ЗафиксироватьТранзакцию() снаружи | 0 | фиксация — или откат, если выше был признак отмены |
Отсюда два следствия. Откатить «только свою часть» нельзя: отмена на любом уровне отменяет всё. А функция ТранзакцияАктивна() говорит лишь о том, что транзакция идёт, но не о том, кто её открыл: во вложенном вызове она всегда вернёт Истина.
Если внутри транзакции произошла ошибка базы данных — не прошла запись, не удалось установить блокировку, упал запрос, — транзакция становится испорченной. Неважно, перехватили вы исключение или нет: продолжить и зафиксировать её нельзя, остаётся только отменить. Следующее обращение к базе закончится ошибкой «В данной транзакции уже происходили ошибки!». Исключения, не связанные с базой, например собственный ВызватьИсключение, транзакцию не портят, но строить на этом прикладную логику не стоит.
Код, который до сих пор встречается в обработках:
// НЕПРАВИЛЬНО: ошибка записи «проглочена» внутри транзакции
НачатьТранзакцию();
Для Каждого Ссылка Из МассивСсылок Цикл
Попытка
Объект = Ссылка.ПолучитьОбъект();
Объект.Записать();
Исключение
// Представление ссылки - это чтение из базы,
// а транзакция после ошибки записи уже испорчена.
Сообщить("Не записан: " + Ссылка);
КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию();
Первая неудачная запись испортит транзакцию. Строка с Сообщить упадёт сразу за ней: чтобы получить представление ссылки, платформа читает базу. Это исключение возникает уже в блоке Исключение, его никто не ловит, и до ЗафиксироватьТранзакцию() дело не доходит — транзакция остаётся незавершённой и в итоге откатывается. Если же такой цикл вызван внутри чужой транзакции, ошибка всплывёт выше по стеку в месте, которое трудно связать с настоящей причиной.
Если задача — «обработать всё, что получится, и показать список ошибок», транзакцию открывают на каждый объект отдельно, а внешней транзакции быть не должно:
// Обрабатывает всё, что получится; ошибки возвращает списком.
// Каждый объект - в своей транзакции, внешней транзакции быть не должно.
Функция ИзменитьПоОдному(МассивСсылок, ЗначенияРеквизитов) Экспорт
Если ТранзакцияАктивна() Тогда
ВызватьИсключение НСтр("ru = 'Функцию нельзя вызывать внутри внешней транзакции'");
КонецЕсли;
Ошибки = Новый Массив;
Для Каждого Ссылка Из МассивСсылок Цикл
ОдинОбъект = Новый Массив;
ОдинОбъект.Добавить(Ссылка);
Попытка
ИзменитьРеквизитыОбъектов(ОдинОбъект, ЗначенияРеквизитов);
Исключение
// Транзакция уже отменена внутри вызова, к базе обращаться можно.
Ошибки.Добавить(СтрШаблон("%1: %2", Ссылка,
ОбработкаОшибок.КраткоеПредставлениеОшибки(ИнформацияОбОшибке())));
КонецПопытки;
КонецЦикла;
Возврат Ошибки;
КонецФункции
После ошибки внутренний вызов уже отменил свою транзакцию, снаружи транзакций нет — поэтому к базе обращаться можно, и представление ссылки в тексте ошибки получится. Проверка в начале функции защищает от вызова внутри чужой транзакции, где такая схема не работает. По той же логике строят повтор после конфликта блокировок: повторять имеет смысл только всю транзакцию целиком и только если перед её началом ТранзакцияАктивна() возвращала Ложь.
Попытка … Исключение без ВызватьИсключение. Удобно ловить остановкой по ошибке в отладчике.НачатьТранзакцию() стоит внутри Попытка после кода, который упал раньше, или транзакцию начинают в одной процедуре, а завершают в другой. Стандарт требует начинать и завершать транзакцию в одном методе.НачатьТранзакцию(), большие объёмы режьте на порции.Возврат между началом и фиксацией. Транзакция остаётся открытой, держит блокировки и в итоге откатывается вместе со всем, что успели записать.Если ТранзакцияАктивна() Тогда ОтменитьТранзакцию(); — страховка, которая ничего не страхует: во вложенном вызове условие истинно из-за внешней транзакции, а при нарушенной парности лишняя отмена снимет чужой уровень счётчика.Сообщить(ОписаниеОшибки()) вместо ВызватьИсключение. Вызывающий код не узнаёт об ошибке и продолжает работу, будто всё записано; внешняя транзакция при этом уже не может быть зафиксирована.Когда в одну неделимую операцию входят несколько записей или когда перед записью нужно заблокировать и прочитать данные. Для записи одного нового объекта явная транзакция не нужна: Записать() сама откроет и закроет её.
Нет. Эти обработчики уже выполняются внутри транзакции записи. Чтобы отменить запись и всё, что сделано в обработчиках, установите Отказ = Истина.
Нет. Точек сохранения во встроенном языке нет, отмена на любом уровне отменяет транзакцию целиком. Если части операции независимы, выполняйте их отдельными транзакциями.
Внешние действия — письма, файлы, HTTP-запросы — транзакция не откатывает, поэтому их выполняют после фиксации. Записи журнала регистрации тоже остаются, но у них сохраняется статус транзакции, по которому видно, что изменения были отменены.
Стандарт 1С ориентирует на порции, запись которых укладывается в 20 секунд в нагруженной базе, — столько по умолчанию другие сеансы ждут блокировку. Долгие групповые операции выносите в фоновое задание и фиксируйте порциями.
Если после доработок начались конфликты блокировок, при проведении или групповой обработке появляется «В данной транзакции уже происходили ошибки», а пользователи жалуются, что 1С тормозит, причина часто в длинных или неправильно вложенных транзакциях. Такие случаи разбирают в рамках услуги «1С тормозит». Опишите симптомы и разместите задачу — оценка бесплатная, ответ в течение рабочего дня.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.