Рассмотрим достаточно частую и с первого взгляда простую задачу в разработке на платформе 1С:Предприятие. Представим ситуацию: при записи первого документа (Док1) нам нужно программно создать, записать и попытаться провести второй документ (Док2). При этом существует жесткое требование — Док1 должен в любом случае остаться записанным, вне зависимости от того, успешно ли прошло проведение Док2. Ошибки проведения второго документа не должны откатывать запись первого.
Проанализируем классическую ошибку, которую часто допускают разработчики. Посмотрим на пример кода, который пишут в обработчике ПриЗаписи модуля объекта первого документа:
Процедура ОбработкаПроведения(Отказ, Режим)
// Создаем или получаем Док2...
Док2.Записать(РежимЗаписиДокумента.Запись);
Попытка
Док2.Записать(РежимЗаписиДокумента.Проведение);
Исключение
// Игнорируем ошибку проведения
КонецПопытки;
КонецПроцедуры
На первый взгляд, конструкция Попытка ... Исключение должна перехватить ошибку и позволить транзакции первого документа успешно завершиться. Но на практике мы получаем ошибку СУБД, и откатывается неявная транзакция записи Док1. Выясним причину такого поведения.
Разберем механику транзакционной базы данных. Обработчики ПередЗаписью, ПриЗаписи и ОбработкаПроведения выполняются в рамках единой транзакции записи основного документа. Когда внутри этой транзакции вызывается запись или проведение другого объекта (Док2), платформа не создает новую независимую транзакцию, а включает эти действия в текущую.
Если при проведении Док2 возникает фатальная ошибка (нехватка остатков, незаполненные реквизиты, программный отказ), платформа помечает всю текущую транзакцию СУБД как "не подлежащую фиксации" (doomed transaction). Конструкция Попытка перехватывает исключение на уровне встроенного языка 1С, но статус транзакции в базе данных уже изменен. При попытке завершить обработчик и зафиксировать транзакцию `Commit` происходит системная ошибка, так как транзакция уже была скомпрометирована, и выполняется принудительный Rollback.
Вывод: реализовать "честную" синхронную запись Док2 внутри транзакции Док1 без риска отката последней невозможно по фундаментальным принципам работы транзакционных СУБД.
Поскольку остаться в рамках одной сессии и транзакции нельзя, любое решение подразумевает создание второго потока или сеанса. Рассмотрим подробнее архитектурно верные и альтернативные подходы к решению данной задачи.
Это наиболее методически верный путь в архитектуре 1С (Event-driven подход), который гарантирует целостность системы.
Вместо попытки провести Док2 непосредственно в коде, мы используем транзакцию Док1 только для фиксации намерения. Разберем по шагам:
ОчередьСобытийПроведения.ПриЗаписи документа Док1 мы формируем и записываем только Док2 (без проведения), а в регистр сведений добавляем запись с ссылкой на этот Док2. Поскольку это простая запись в таблицы, она пройдет успешно и зафиксируется вместе с Док1.Если необходимо попытаться провести документ максимально быстро, можно использовать механизм ФоновыеЗадания. Разберем особенности этого метода.
Из модуля Док1 (лучше в обработчике ПриЗаписи, после того как данные первого документа помещены в базу, но до конца транзакции) инициируется фоновое задание, в которое передаются нужные параметры.
ПараметрыЗадания = Новый Массив;
ПараметрыЗадания.Добавить(Док2.Ссылка);
ФоновыеЗадания.Выполнить("ОбщийМодульПроведения.ПровестиДокументВФоне", ПараметрыЗадания);
Фоновое задание запускается в отдельном сеансе на сервере 1С. У него будет своя независимая транзакция. Если Док2 не проведется, транзакция Док1 никак не пострадает. Однако здесь мы сталкиваемся с проблемой "Грязного чтения" (Dirty Read).
В БД 1С используется уровень изоляции транзакций Read Committed. Это означает, что фоновое задание, стартовавшее параллельно, не увидит изменений, сделанных в Док1, пока транзакция Док1 полностью не зафиксируется. Поэтому, если для проведения Док2 критически важны новые данные из Док1, фоновое задание может отработать некорректно или "упасть" из-за блокировок. В таких случаях нужно использовать передачу контекста (структуры с данными) напрямую в параметры фонового задания, а не полагаться на чтение Док1 из базы.
Современный метод обхода ограничения — вызов собственного HTTP-сервиса базы.
В обработчике документа инициализируется HTTP-запрос к этой же базе данных. Платформа принимает запрос и создает для него новый сеанс со своей независимой транзакцией. Это позволяет выполнить действие синхронно: код Док1 будет ожидать ответа от HTTP-сервиса (провелся Док2 или нет). При этом весь вызов оборачивается в конструкцию Попытка - Исключение, и ошибка HTTP-сервиса не "ломает" транзакцию Док1.
Минус подхода — дополнительные накладные расходы на инициализацию сеанса и сетевое взаимодействие, а также та же проблема изоляции Read Committed, как и в фоновых заданиях.
Старый аналог подхода с HTTP-сервисами. Из кода создается внутреннее COM-соединение V83.COMConnector с этой же базой. Через полученный COM-объект вызывается процедура проведения Док2. Так как это новый сеанс, транзакция независима.
В современных реалиях метод не рекомендуется: он расходует клиентские лицензии, работает медленнее, зависит от разрядности библиотек и настроек безопасности сервера ОС.
Иногда правильным решением является пересмотр архитектуры. Если закрытый код, записывающий Док1, недоступен для изменения, но вы имеете контроль над тем местом, которое этот код вызывает, логику работы с Док2 стоит вынести именно туда. Сначала мы позволяем закрытому коду полностью отработать (записать Док1 и завершить транзакцию), и только потом, следующим шагом в клиентском или серверном вызове, создаем и проводим Док2. Если формы нет, это можно обернуть в единый серверный метод-фасад.