При работе с платформой 1С:Предприятие 8.3 разработчики часто сталкиваются с необходимостью выполнять операции, требующие атомарности данных. В некоторых случаях это приводит к попыткам инициировать новую транзакцию, когда текущий сеанс сервера уже находится внутри другой транзакции. Такая ситуация вызывает критическую ошибку: "Ошибка при вызове метода контекста (НачатьТранзакцию)". Давайте подробно разберем, почему возникает эта ошибка и как ее корректно преодолеть, используя механизмы платформы 1С.
Мы рассмотрим типичную ситуацию, когда вы пытаетесь вызвать процедуру, которая содержит НачатьТранзакцию(), из контекста, который уже находится в транзакции. Затем мы выясним причины возникновения этой ошибки и предложим наиболее эффективное и безопасное решение.
Предположим, у нас есть основной код, который выполняет некоторую логику в транзакции:
НачатьТранзакцию();
Попытка
// Какие-то действия, относящиеся к основной транзакции
Документ.Записать();
// Вызов процедуры, которая также содержит НачатьТранзакцию()
МодульОбъектаМенеджера.ПроведениеДокументов(МассивДокументов, ПараметрыПроведения);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
А внутри процедуры МодульОбъектаМенеджера.ПроведениеДокументов() мы видим следующую структуру:
Процедура ПроведениеДокументов(СписокДокументов, ПараметрыПроведения) Экспорт
НачатьТранзакцию(); // Здесь возникает ошибка "Ошибка при вызове метода контекста (НачатьТранзакцию)"
Попытка
// ... Логика проведения документов ...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
При попытке выполнить такой код платформа 1С:Предприятие выдает ошибку. Причина кроется в архитектуре транзакций в 1С. Мы должны понимать, что транзакции в 1С являются глобальными по отношению к серверному сеансу. Это означает, что в рамках одного и того же серверного сеанса может быть активна только одна транзакция. Когда вы пытаетесь вызвать НачатьТранзакцию() внутри уже запущенной транзакции в том же сеансе, платформа интерпретирует это как попытку начать новую, независимую транзакцию, в то время как одна уже активна. Это не является вложенной транзакцией в классическом понимании большинства СУБД (где вложенные транзакции обычно фиксируются или откатываются вместе с родительской транзакцией), а скорее попыткой "перезапустить" транзакцию, что запрещено.
Для обеспечения атомарности и целостности данных, когда нам действительно требуется выполнить часть логики в собственной, изолированной транзакции, независимой от вызывающего контекста, нам необходимо использовать механизмы, позволяющие выполнить эту логику в другой серверной сессии.
Наиболее корректным и распространенным способом решения проблемы "вложенных" транзакций в 1С является выполнение внутренней операции в отдельном серверном сеансе. Платформа 1С предоставляет для этого механизм фоновых заданий. Каждое фоновое задание запускается в собственном, независимом сеансе, что автоматически обеспечивает необходимую изоляцию для транзакций.
Рассмотрим пошагово, как реализовать это решение.
Первым делом нам понадобится процедура, которая будет выполнять "вложенную" логику (в нашем случае, проведение документов) и содержать свою собственную транзакцию. Эта процедура должна быть экспортной, чтобы ее можно было вызвать из фонового задания. Разместим ее, например, в модуле менеджера соответствующего документа или в общем модуле, если она является универсальной.
Мы создадим процедуру ПровестиДокументВОтдельнойТранзакции, которая будет принимать идентификатор документа и, при необходимости, другие параметры для проведения.
Пример кода экспортной процедуры:
// В модуле менеджера документа (например, Документы.МойДокумент.МодульМенеджера)
// или в подходящем общем модуле с установленным свойством "Сервер" и "Вызов сервера"
Процедура ПровестиДокументВОтдельнойТранзакции(ИДДокумента, ПараметрыПроведения) Экспорт
// Начинаем собственную транзакцию для фонового задания
НачатьТранзакцию();
Попытка
// Получаем объект документа по ссылке
ДокументОбъект = ИДДокумента.ПолучитьОбъект();
// Здесь вызываем исходную процедуру проведения документов,
// которая теперь будет выполняться в своей изолированной транзакции
Если ДокументОбъект <> Неопределено Тогда
МодульОбъектаМенеджера = ДокументОбъект.Метаданные().МодульМенеджера;
Если МодульОбъектаМенеджера.Методы.Содержит("ПроведениеДокументов") Тогда
// Предполагаем, что МодульОбъектаМенеджера доступен или получаем его ссылку
// Здесь вызываем процедуру, которая изначально вызывала ошибку.
// Она теперь будет исполнена в новой сессии фонового задания.
МодульОбъектаМенеджера.ПроведениеДокументов(Новый Массив(), ПараметрыПроведения);
Иначе
// Если нет специфической процедуры ПроведениеДокументов, используем стандартную запись
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
КонецЕсли;
// Фиксируем транзакцию фонового задания
ЗафиксироватьТранзакцию();
Исключение
// В случае ошибки, отменяем транзакцию фонового задания
ОтменитьТранзакцию();
// Важно явно вызвать исключение с подробным описанием,
// чтобы статус фонового задания отразил ошибку
ВызватьИсключение "Ошибка проведения документа в фоновом задании: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Обратите внимание на ключевые моменты в этой процедуре:
Экспорт: Процедура должна быть экспортной, чтобы ее можно было вызвать через ФоновыеЗадания.Выполнить.НачатьТранзакцию(): Здесь мы начинаем новую транзакцию, но поскольку эта процедура будет выполняться в отдельной сессии фонового задания, эта транзакция будет первой и единственной в этом новом сеансе, не конфликтуя с основной.Попытка...Исключение мы обеспечиваем корректную фиксацию или отмену транзакции фонового задания. Важно использовать ВызватьИсключение, чтобы основное задание могло отследить результат выполнения фонового задания.Теперь, в основном коде, где раньше происходила ошибка, мы заменим прямой вызов процедуры ПроведениеДокументов на запуск фонового задания. Мы создадим структуру параметров, передадим в нее необходимые данные (например, ссылку на документ и параметры проведения) и используем метод ФоновыеЗадания.Выполнить().
Пример вызова фонового задания:
Процедура ВыполнитьОсновнуюОперацию()
// Основная транзакция
НачатьТранзакцию();
Попытка
// Некоторые действия основной транзакции
НовыйДокумент = Документы.МойДокумент.СоздатьДокумент();
// ... заполнение реквизитов НовогоДокумента ...
НовыйДокумент.Записать(); // Пример записи документа в основной транзакции
// Подготовка параметров для фонового задания
ПараметрыДляФоновогоЗадания = Новый Структура();
ПараметрыДляФоновогоЗадания.Вставить("ИДДокумента", НовыйДокумент.Ссылка); // Передаем ссылку на документ
// Добавляем параметры, которые необходимы для процедуры проведения
ПараметрыДляФоновогоЗадания.Вставить("ПараметрыПроведения", ПараметрыПроведения);
// Определяем имя метода, который будет выполняться в фоновом задании
// Указываем полное имя: "ИмяОбщегоМодуля.ИмяЭкспортнойПроцедуры"
// или "Документы.ИмяДокумента.МодульМенеджера.ИмяЭкспортнойПроцедуры"
ИмяМетодаФоновогоЗадания = "Документы.МойДокумент.МодульМенеджера.ПровестиДокументВОтдельнойТранзакции";
// Запускаем фоновое задание
ФоновоеЗадание = ФоновыеЗадания.Выполнить(ИмяМетодаФоновогоЗадания, ПараметрыДляФоновогоЗадания);
// *** Важный момент: Обработка результатов фонового задания ***
// В зависимости от логики, вы можете:
// 1. Продолжить выполнение основной транзакции асинхронно,
// не дожидаясь завершения фонового задания.
// 2. Дождаться завершения фонового задания, если результат критичен
// для дальнейших действий в основной транзакции.
// Рассмотрим вариант с ожиданием для синхронной обработки:
// Ждем завершения фонового задания
Пока ФоновоеЗадание.Состояние = СостояниеФоновогоЗадания.Выполняется Цикл
// Можно добавить задержку для снижения нагрузки на процессор
// и для более частых проверок статуса.
// Например, УстановитьТаймер(100); или просто Продолжить;
Продолжить;
КонецЦикла;
// Проверяем результат выполнения фонового задания
Если ФоновоеЗадание.Состояние = СостояниеФоновогоЗадания.Выполнено Тогда
Сообщить("Документ " + НовыйДокумент.Представление() + " успешно проведен в отдельной транзакции.");
// Если фоновое задание возвращало какие-либо данные, их можно получить:
// РезультатФоновогоЗадания = ФоновоеЗадание.ПолучитьРезультат();
Иначе
// Обработка ошибки выполнения фонового задания
Сообщить("Ошибка проведения документа " + НовыйДокумент.Представление() + " в фоновом задании: " + ФоновоеЗадание.ОписаниеОшибки);
// Если фоновое задание завершилось с ошибкой, мы можем решить:
// 1. Отменить всю основную транзакцию.
// 2. Зафиксировать основную транзакцию, а ошибку обработки в фоновом задании
// обработать как отдельное событие (например, записать в журнал).
// В данном примере мы вызываем исключение для отмены основной транзакции.
ВызватьИсключение "Не удалось провести документ в отдельной транзакции.";
КонецЕсли;
// Фиксируем основную транзакцию
ЗафиксироватьТранзакцию();
Исключение
// Отменяем основную транзакцию в случае любой ошибки
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Здесь мы видим следующие важные аспекты:
Структура или Массив для передачи нескольких параметров.ФоновыеЗадания.Выполнить() запускает фоновое задание. Он принимает имя экспортной процедуры и параметры.ФоновыеЗадания.Выполнить() основной код продолжает выполняться, не дожидаясь завершения фонового задания. Если нам необходимо дождаться результата, как в нашем примере, мы можем реализовать цикл ожидания, проверяя ФоновоеЗадание.Состояние. Однако, при этом стоит помнить, что длительное ожидание может блокировать текущий сеанс, что не всегда желательно.Состояние и, если оно выполнено с ошибкой, получить ОписаниеОшибки для дальнейшей обработки.Таким образом, мы добиваемся необходимой изоляции: основная транзакция выполняется в одном сеансе, а проведение документа со своей собственной транзакцией – в совершенно другом, изолированном сеансе, запускаемом фоновым заданием. Это полностью исключает конфликт вызовов НачатьТранзакцию().
В некоторых случаях, когда вам не требуется строгая атомарность на уровне СУБД для "вложенной" операции, а нужно лишь избежать конфликтов записи данных, можно рассмотреть использование блокировок данных, предоставляемых платформой 1С. Этот подход не создает новую транзакцию, а лишь управляет доступом к данным.
Мы используем объект БлокировкаДанных для явной блокировки таблиц или записей, к которым обращается процедура ПроведениеДокументов. Это позволяет предотвратить одновременное изменение одних и тех же данных из разных сеансов.
Процедура ПроведениеДокументовСБлокировкой(СписокДокументов, ПараметрыПроведения) Экспорт
Блокировка = Новый БлокировкаДанных();
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров"); // Пример блокировки регистра
ЭлементБлокировки.УстановитьРежим(РежимБлокировкиДанных.Исключительный);
// Можно добавить и другие элементы блокировки для связанных данных
Попытка
Блокировка.Заблокировать();
// ... Логика проведения документов без НачатьТранзакцию() ...
Исключение
ВызватьИсключение "Ошибка проведения документов с блокировкой: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
В этом случае, вместо НачатьТранзакцию(), мы используем Блокировка.Заблокировать(). Это обеспечивает эксклюзивный доступ к данным, но не создает новую транзакцию. Данные, измененные в этой процедуре, станут видимыми только после фиксации основной транзакции, из которой была вызвана ПроведениеДокументовСБлокировкой.
Важно: Этот подход подходит, если вам не нужна независимая атомарность "вложенной" операции (например, чтобы она могла быть отменена независимо), а достаточно лишь исключить конфликты записи. Для таких операций как проведение документов, которые часто модифицируют множество связанных данных и должны быть атомарными, использование фоновых заданий с собственными транзакциями является предпочтительным.
При использовании фоновых заданий для изоляции транзакций, мы должны учитывать несколько важных моментов, чтобы обеспечить стабильность и корректность работы системы.
Как мы уже выяснили, фоновые задания асинхронны. Если логика вашего приложения требует немедленного результата выполнения фонового задания (как в примере выше, где мы используем цикл Пока ФоновоеЗадание.Состояние = СостояниеФоновогоЗадания.Выполняется), это фактически превращает асинхронную операцию в синхронную. Мы должны с осторожностью подходить к такому ожиданию. Длительные циклы ожидания могут блокировать текущий сеанс пользователя и приводить к снижению производительности. Для интерактивных операций, если фоновое задание может выполняться долго, рекомендуется пересмотреть архитектуру: например, запустить фоновое задание и уведомлять пользователя о его завершении через механизм оповещений или обновлять данные в отчетах/формах по мере готовности.
Исключения, возникающие в фоновом задании, должны быть корректно обработаны внутри этого задания. Использование конструкции Попытка...Исключение и последующего ВызватьИсключение позволяет платформе записать описание ошибки в свойство ОписаниеОшибки объекта ФоновоеЗадание. Это критично для отладки и понимания причин сбоев. В основном коде мы затем проверяем это свойство, чтобы реагировать на неуспешное выполнение.
Это один из наиболее важных моментов. Изменения, сделанные в основной транзакции, не будут видны фоновому заданию до тех пор, пока основная транзакция не будет зафиксирована (ЗафиксироватьТранзакцию()). Если фоновое задание должно работать с данными, которые были изменены или созданы в текущей (основной) транзакции, мы должны убедиться, что основная транзакция зафиксирована *до* того, как фоновое задание попытается получить эти данные. В противном случае, фоновое задание может работать с устаревшими данными или вообще не найти созданные объекты. Если же вам нужны данные из основной транзакции для фонового задания, но при этом основная транзакция еще не должна быть зафиксирована, эти данные необходимо передать в фоновое задание как параметры (например, ИДДокумента, ЗначенияРеквизитов), а не пытаться прочитать их из базы данных внутри фонового задания.
Все параметры, передаваемые в фоновое задание, должны быть сериализуемыми. Это означает, что они должны быть простыми типами (строки, числа, даты, булево), ссылками на объекты 1С, а также структурами, массивами, таблицами значений, Соответствиями, содержащими сериализуемые типы. Объекты, которые нельзя сериализовать (например, формы, элементы управления, менеджеры объектов), не могут быть переданы напрямую. Если вам нужно передать сложный объект, передавайте его ссылку или ключевые поля, по которым объект может быть восстановлен в сессии фонового задания.
Отладка кода, выполняющегося в фоновом задании, требует специальных настроек. Мы должны подключиться отладчиком к процессу фоновых заданий. Обычно это делается через меню "Отладка" -> "Подключиться" в конфигураторе, выбирая процесс с типом "Фоновое задание" или "Сервер 1С:Предприятия", если фоновые задания выполняются в том же рабочем процессе сервера.
Фоновые задания выполняются на сервере и не имеют прямого доступа к клиентскому интерфейсу. Они не могут выводить сообщения пользователю через Сообщить() или Предупреждение(). Для информирования пользователя о ходе выполнения или результате необходимо использовать механизмы уведомлений (например, подписки на события фоновых заданий, обновление данных для отображения в пользовательском интерфейсе после завершения задания).
Запуск большого количества фоновых заданий может создать значительную нагрузку на сервер. Мы должны проектировать систему так, чтобы количество одновременно выполняющихся заданий было управляемым и соответствовало ресурсам сервера. В 1С есть механизмы управления фоновыми заданиями, позволяющие ограничивать их количество и назначать расписание.
Мы выяснили, что прямые "вложенные" вызовы НачатьТранзакцию() в одной серверной сессии в 1С:Предприятии 8.3 приводят к ошибке из-за специфики реализации транзакций на платформе. Наиболее надежным и правильным решением этой проблемы является изоляция "внутренней" транзакции путем ее выполнения в отдельном серверном сеансе с помощью фонового задания. Этот подход обеспечивает атомарность и целостность данных для каждой операции, не допуская конфликтов транзакций.
Также мы рассмотрели альтернативный подход с использованием объектных блокировок, который может быть применен в случаях, когда не требуется полная атомарность на уровне СУБД, а достаточно исключить параллельную запись. Однако для таких операций, как проведение документов, фоновые задания остаются предпочтительным выбором.
Применяя эти знания и следуя лучшим практикам по работе с фоновыми заданиями и транзакциями, мы сможем создавать более robustные и надежные решения на платформе 1С:Предприятие 8.3.