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