Каждый разработчик на платформе «1С:Предприятие» рано или поздно сталкивается с ситуацией, когда код, написанный в обработчике проведения документа, ведет себя неочевидным образом. Типичный пример: внутри процедуры ОбработкаПроведения выполняется запись в абсолютно независимый, непериодический регистр сведений. Казалось бы, регистр никак не связан с документом (не подчинен ему по регистратору), запись в базу данных физически происходит через метод Записать(). Но как только платформа доходит до строки Отказ = Истина, все внесенные изменения в регистре чудесным образом исчезают.
Давайте подробно разберем, почему так происходит с точки зрения архитектуры СУБД и платформы 1С, проанализируем внутренние механизмы транзакций и рассмотрим несколько практических способов обойти это ограничение в зависимости от стоящих перед нами задач.
Для того чтобы понять причину происходящего, нам необходимо обратиться к понятию транзакции базы данных. Транзакция — это неделимая группа последовательных операций с данными, которая может быть либо выполнена целиком и успешно (коммит, COMMIT), либо полностью отменена (откат, ROLLBACK). Этот механизм гарантирует целостность базы данных (соответствие принципам ACID).
В платформе «1С:Предприятие» при проведении документа работа с транзакциями устроена следующим образом:
ОбработкаПроведения платформа автоматически и неявно для разработчика открывает транзакцию СУБД.Отказ присваивается значение Истина, платформа расценивает это как сигнал о невозможности успешного завершения бизнес-логики.ROLLBACK для открытой транзакции. СУБД послушно отменяет абсолютно все изменения в таблицах, которые были произведены с момента открытия этой транзакции. Под откат попадает и ваш независимый регистр сведений.В платформе 1С не существует механизма автономных (вложенных независимых) транзакций. Вы не можете внутри процедуры ОбработкаПроведения сказать платформе: «Запиши вот эти данные в регистр так, чтобы их не затронул общий откат». Если транзакция откатывается, откатывается абсолютно всё.
Тем не менее, решить задачу фиксации данных (например, для ведения истории попыток проведения, аудита действий пользователей или фиксации ошибок) необходимо. Рассмотрим детально рабочие архитектурные варианты решения этой проблемы.
Самый правильный и архитектурно чистый подход — не начинать транзакцию проведения, если мы заранее знаем, что она может завершиться ошибкой. Нам нужно выполнить проверку возможности проведения еще до того, как СУБД заблокирует таблицы.
Рассмотрим, как это реализовать. Вместо выполнения проверки непосредственно внутри ОбработкаПроведения, мы можем перенести её в обработчик формы или сделать вызов специального метода перед записью.
Если проверка не пройдена, мы программно записываем нужную информацию в независимый регистр в рамках обычной (короткой) транзакции записи регистра, а само проведение документа даже не запускаем. Посмотрим на пример кода в модуле формы документа:
&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
Если ПараметрыЗаписи.РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
// Выполняем предварительную проверку на сервере
РезультатПроверки = ПроверитьВозможностьПроведенияНаСервере();
Если Не РезультатПроверки.Разрешено Тогда
// Фиксируем неудачную попытку в независимом регистре
ЗаписатьПопыткуВРегистрНаСервере(РезультатПроверки.ОписаниеОшибки);
// Отменяем запись/проведение на клиенте
Отказ = Истина;
ПоказатьПредупреждение(, "Документ не может быть проведен: " + РезультатПроверки.ОписаниеОшибки);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Преимущества: Полное отсутствие избыточных блокировок СУБД, экономия серверных ресурсов, данные в независимом регистре гарантированно сохраняются, так как транзакция проведения даже не открывалась.
Ограничения: Этот способ работает преимущественно при интерактивном проведении пользователем из формы документа. Если документ проводится программно (например, при обмене данными или групповой обработке), этот клиентский обработчик выполнен не будет.
Если ваша задача — аудит (например, фиксация первого пользователя, пытавшегося провести документ, или запись ошибок проведения для последующего анализа разработчиками), идеальным решением будет связка Журнала регистрации и базы данных.
Дело в том, что Журнал регистрации пишется платформой 1С на уровне операционной системы (в отдельные файлы логов или системную таблицу) напрямую, минуя общую транзакцию СУБД. Записи в Журнале регистрации никогда не откатываются при отмене транзакции проведения.
Разберем по шагам, как настроить этот процесс:
Шаг 1. В процедуре ОбработкаПроведения записываем событие в Журнал регистрации:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Какая-то логика, приведшая к отказу
Если Не ПроверкаПройдена() Тогда
// Формируем структуру данных для записи
ДанныеДляЛога = Новый Структура;
ДанныеДляЛога.Вставить("ДокументСсылка", Ссылка);
ДанныеДляЛога.Вставить("Пользователь", ПараметрыСеанса.ТекущийПользователь);
ДанныеДляЛога.Вставить("ДатаПопытки", ТекущаяДата());
// Преобразуем в строку JSON для удобства хранения и последующего парсинга
ЗаписьЗаписи = Новый ЗаписьJSON;
ЗаписьЗаписи.УстановитьСтроку();
ЗаписатьJSON(ЗаписьЗаписи, ДанныеДляЛога);
СтрокаДанных = ЗаписьЗаписи.Закрыть();
// Записываем в ЖР. Этот вызов НЕ откатится при Отказ = Истина!
ЗаписатьВЖурналРегистрации(
"Документы.ПопыткаПроведения",
УровеньЖурналаРегистрации.Предупреждение,
Метаданные.Документы.артДокументПродажи,
Ссылка,
СтрокаДанных
);
Отказ = Истина;
Возврат;
КонецЕсли;
КонецПроцедуры
Шаг 2. Создаем регламентное задание, которое будет запускаться по расписанию (например, раз в 5–10 минут), считывать новые записи из Журнала регистрации по нашему событию "Документы.ПопыткаПроведения" и переносить их в наш независимый регистр сведений уже вне каких-либо транзакций проведения документов.
Преимущества: Чрезвычайно высокая надежность. Журнал регистрации гарантирует запись данных без создания дополнительных нагрузок и транзакционных блокировок в процессе проведения документа.
Ограничения: Данные в независимом регистре сведений появляются не мгновенно, а с небольшой задержкой, определяемой расписанием регламентного задания (асинхронная модель).
Поскольку фоновое задание выполняется в отдельном сеансе на сервере 1С, оно работает в своей собственной независимой транзакции базы данных. Если мы запустим фоновое задание из обработчика проведения, оно успешно запишет данные в регистр сведений, и откат основной транзакции проведения документа никак не повлияет на транзакцию фонового задания.
Однако здесь кроется критически опасная техническая ловушка, называемая «грязным чтением» (Dirty Read) или взаимной блокировкой.
Если вы передадите в фоновое задание ссылку на проводимый документ и внутри фонового задания попытаетесь прочитать реквизиты этого документа из базы данных, возникнет проблема. Основная транзакция проведения еще не завершена (и не откачена), она удерживает блокировку на запись документа. Фоновое задание попытается обратиться к документу и:
Правильное решение: Передавать в параметры фонового задания только простые типы данных (числа, строки, даты, уникальные идентификаторы, структуры), чтобы фоновому процессу вообще не требовалось обращаться к базе данных для получения информации о текущем состоянии документа.
Посмотрим на пример правильной реализации:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Предположим, проверка не пройдена
Если Не ПроверкаПройдена() Тогда
// Подготавливаем параметры для фонового задания
ПараметрыПроцедуры = Новый Массив;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("ДокументСсылка", Ссылка);
ПараметрыЗаписи.Вставить("ДатаПопытки", ТекущаяДата());
ПараметрыЗаписи.Вставить("ПользовательСсылка", ПараметрыСеанса.ТекущийПользователь);
ПараметрыПроцедуры.Добавить(ПараметрыЗаписи);
// Запускаем фоновое задание. Оно выполнится в своем сеансе и своей транзакции.
ФоновыеЗадания.Выполнить(
"ОбщийМодульСервер.ЗаписатьДанныеОПопыткеПроведенияАсинхронно",
ПараметрыПроцедуры
);
Отказ = Истина;
Возврат;
КонецЕсли;
КонецПроцедуры
В общем модуле ОбщийМодульСервер (свойства: Сервер, Вызов сервера) размещаем процедуру:
Процедура ЗаписатьДанныеОПопыткеПроведенияАсинхронно(ПараметрыЗаписи) Экспорт
// Внимание: внутри этой процедуры мы работаем ТОЛЬКО со свойствами структуры "ПараметрыЗаписи"!
// Мы НЕ обращаемся к базе данных для чтения реквизитов документа ПараметрыЗаписи.ДокументСсылка.
МенеджерЗаписи = РегистрыСведений.артДанныеОПроведенииДокументов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Объект = ПараметрыЗаписи.ДокументСсылка;
МенеджерЗаписи.ДатаПервойПопыткиПроведения = ПараметрыЗаписи.ДатаПопытки;
МенеджерЗаписи.ПервыйКтоПыталсяПровестиДокумент = ПараметрыЗаписи.ПользовательСсылка;
Попытка
МенеджерЗаписи.Записать(Истина);
Исключение
// Обязательно обрабатываем возможные исключения блокировок
ЗаписатьВЖурналРегистрации(
"ФоновоеЗадание.ОшибкаЗаписи",
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())
);
КонецПопытки;
КонецПроцедуры
Преимущества: Быстрый обход транзакционного механизма проведения, данные появляются в регистре практически мгновенно.
Ограничения: Создание нового сеанса фонового задания — ресурсоемкая операция для сервера 1С. При массовом проведении документов (например, групповое перепроведение за месяц) этот способ быстро исчерпает свободные рабочие процессы сервера и критически снизит производительность всей системы. Не рекомендуется для высоконагруженных систем при частых транзакциях.
Существует еще один способ «разорвать» транзакционный контекст текущего сеанса — отправить HTTP-запрос к веб-сервису, опубликованному на этой же базе 1С. Поскольку HTTP-сервис обрабатывает входящие запросы в отдельном пуле соединений со своими собственными сеансами и транзакциями, любая запись, сделанная внутри обработчика HTTP-сервиса, успешно зафиксируется в БД.
Однако вы должны помнить о строжайших правилах разработки фирмы «1С». Крайне не рекомендуется выполнять внешние вызовы (HTTP, Web-сервисы, FTP, файлы) изнутри транзакции.
Если веб-сервис зависнет или возникнет сетевой сбой, ваша транзакция проведения останется открытой до тех пор, пока не сработает таймаут соединения (обычно до 30 секунд). Все это время таблицы базы данных будут заблокированы, что приведет к полной остановке работы всех остальных пользователей системы.
Используйте данный подход только в крайних случаях, обязательно настраивая минимально возможные таймауты (не более 1–2 секунд) для HTTP-запроса:
Соединение = Новый HTTPСоединение("localhost", , , , , 2); // Таймаут 2 секунды
Мы подробно проанализировали причины отката изменений в независимом регистре сведений при установке Отказ = Истина в обработке проведения. Это фундаментальное ограничение СУБД, а не ошибка платформы 1С. Любые действия в рамках одной транзакции неделимы.
Для успешного решения этой задачи используйте один из рассмотренных подходов: