Как правильно изменить связанные документы при проведении и избежать ошибок транзакций в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Разработка логики проведения документов в 1С часто сталкивает программистов с необходимостью изменить данные в сопутствующих, связанных объектах. Типичный пример: при проведении платежного поручения требуется обновить статус оплаты или установить определенный реквизит в исходном заказе покупателя и связанных с ним корректировках.

Однако простое решение «в лоб» — открыть выборку связанных документов в обработчике проведения, изменить реквизиты и записать их через блок Попытка — Исключение — таит в себе серьезные архитектурные угрозы. В этой статье мы подробно разберем, почему стандартные попытки обойти транзакционную логику приводят к фатальным ошибкам СУБД, и рассмотрим лучшие практики и шаблоны проектирования для безопасного обновления связанных данных.

Почему «вложенные» транзакции и блоки Попытка-Исключение внутри проведения — это мина замедленного действия?

Давайте разберем поведение платформы «1С:Предприятие» при попытке управления транзакциями внутри обработчика проведения. Обработка проведения по умолчанию всегда выполняется в рамках одной большой неявной транзакции, которую открывает сама платформа. Попытка написать следующий код:


НачатьТранзакцию();
Попытка
    // Цикл обхода и запись связанных документов
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
КонецПопытки;

является грубой ошибкой по нескольким причинам:

  1. Отсутствие физической вложенности транзакций в СУБД. На уровне базы данных никаких вложенных транзакций не существует. Платформа 1С лишь имитирует их с помощью внутреннего счетчика транзакций. Реальная фиксация данных в БД происходит только тогда, когда этот счетчик равен нулю (то есть закрыта самая внешняя транзакция).
  2. Эффект «битой» транзакции (Broken Transaction). Если при записи зависимого документа происходит ошибка (например, сработала проверка заполнения или документ заблокирован другим пользователем), СУБД помечает текущую транзакцию как нежизнеспособную (статус «Только для отмены»). Даже если вы перехватили эту ошибку в блоке Исключение и решили «заглушить» ее, чтобы продолжить проведение основного документа, СУБД больше не позволит записать ни одного байта информации. Любое последующее обращение к базе данных в рамках этого обработчика вызовет критическую системную ошибку: «В данной транзакции уже происходили ошибки!».
  3. Взаимоблокировки (Deadlocks). Если Пользователь №1 проводит платеж, который блокирует и перезаписывает заказ, а Пользователь №2 в этот же момент проводит этот же самый заказ (который по своей логике обращается к платежу), СУБД зафиксирует клин транзакций и аварийно завершит одну из них. При высокой интенсивности работы пользователей система начнет постоянно зависать.

Ниже мы подробно рассмотрим четыре правильных способа решения этой задачи — от простых к наиболее надежным и масштабируемым.

Решение 1. Доверить управление транзакцией платформе (Простая запись)

Если бизнес-логика требует жесткой консистентности (если не записался хотя бы один связанный документ — платежка тоже не должна проводиться), мы полностью отказываемся от блоков Попытка — Исключение и ручного управления транзакциями. Мы доверяем этот процесс платформе.

В обработчике проведения основного документа мы просто получаем объекты связанных документов, меняем их реквизиты и вызываем метод Записать(). Разберем этот пример на коде:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // Получаем связанные документы (например, запросом)
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ЗаказПокупателя.Ссылка КАК Ссылка
        |ИЗ
        |   Документ.ЗаказПокупателя КАК ЗаказПокупателя
        |ГДЕ
        |   ЗаказПокупателя.ДокументОснование = &ТекущийДокумент";
    
    Запрос.УстановитьПараметр("ТекущийДокумент", ЭтотОбъект.Ссылка);
    РезультатЗапроса = Запрос.Выполнить();
    
    Если Не РезультатЗапроса.Пустой() Тогда
        Выборка = РезультатЗапроса.Выбрать();
        Пока Выборка.Следующий() Цикл
            
            // Получаем объект документа для изменения
            Попытка
                ОбъектСвязи = Выборка.Ссылка.ПолучитьОбъект();
                ОбъектСвязи.РеквизитОплачен = Истина;
                
                // Записываем документ БЕЗ попытки-исключения!
                // Ошибки записи должны приводить к штатному отказу всей транзакции проведения.
                ОбъектСвязи.Записать(РежимЗаписиДокумента.Запись);
            Исключение
                // Сюда мы можем зайти, только чтобы вывести понятное сообщение пользователю,
                // но мы ОБЯЗАНЫ установить Отказ = Истина и пробросить ошибку дальше,
                // чтобы транзакция корректно откатилась.
                Отказ = Истина;
                Сообщение = Новый СообщениеПользователю;
                Сообщение.Текст = "Не удалось обновить связанный документ: " + Выборка.Ссылка + ". Ошибка: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
                Сообщение.Сообщить();
                
                // Вызываем исключение, чтобы не допустить продолжения "битой" транзакции
                ВызватьИсключение "Проведение отменено из-за невозможности обновить связанные документы.";
            КонецПопытки;
            
        КонецЦикла;
    КонецЕсли;

КонецПроцедуры

Важное примечание: Этот вариант подходит только для малонагруженных систем. При одновременной работе десятков пользователей он неизбежно приведет к транзакционным блокировкам.

Решение 2. Использование регистров сведений (Рекомендуемый подход)

Хранить признак (флаг оплаты, статус и т.д.) внутри реквизитов самого документа ЗаказПокупателя — это классическая методологическая ошибка. Из-за этого приходится перезаписывать тяжелые объекты документов, запуская их многочисленные подписки на события и проверки.

Гораздо правильнее создать независимый или подчиненный регистратору Регистр сведений (например, СтатусыОплатыЗаказов). В этом случае проведение платежного поручения будет просто делать запись в этот регистр. Сам заказ при этом физически не изменяется и не блокируется!

Проанализируем пример кода записи в регистр сведений при проведении платежного поручения:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // Создаем набор записей нашего регистра сведений.
    // Если регистр подчинен регистратору, то очистка и запись произойдет автоматически.
    НаборЗаписей = РегистрыСведений.СтатусыОплатыЗаказов.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Регистратор.Установить(ЭтотОбъект.Ссылка);
    
    // Добавляем запись о состоянии связанного заказа
    НоваяСтрока = НаборЗаписей.Добавить();
    НоваяСтрока.Заказ = ЭтотОбъект.Сделка;
    НоваяСтрока.СтатусОплаты = Перечисления.СтатусыОплаты.Оплачен;
    НоваяСтрока.СуммаОплаты = ЭтотОбъект.СуммаДокумента;
    
    // Быстрая запись без блокировки самого объекта "Заказ"
    НаборЗаписей.Записать();

КонецПроцедуры

В форме самого ЗаказаПокупателя этот статус можно динамически считывать быстрым запросом при открытии или отображать на форме через динамический список. Этот подход полностью исключает взаимоблокировки и многократно увеличивает скорость работы системы.

Решение 3. Организация асинхронной очереди задач (Паттерн отложенного обновления)

Если бизнес-логика все же требует жесткого физического изменения реквизитов в связанных документах (например, для обмена с внешними системами), лучшим решением будет использование асинхронного паттерна через очередь задач.

Выясним схему работы этого паттерна:

  1. При проведении платежного поручения мы не меняем связанные документы сразу. Мы лишь делаем быструю запись в специальный регистр-очередь ОчередьОбновленияДокументов.
  2. Специальное регламентное задание (фоновый процесс) по расписанию (например, каждую минуту) считывает новые записи из этой очереди, открывает изолированные транзакции для каждого связанного документа, обновляет их и удаляет обработанные задачи из очереди.

Рассмотрим код регистрации задачи в очереди внутри обработки проведения платежки:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // Быстро регистрируем необходимость обновления документов в очереди
    МенеджерЗаписи = РегистрыСведений.ОчередьОбновленияДокументов.СоздатьМенеджерЗаписи();
    МенеджерЗаписи.ДокументДляОбновления = ЭтотОбъект.Сделка;
    МенеджерЗаписи.ИсточникОбновления = ЭтотОбъект.Ссылка;
    МенеджерЗаписи.ДатаРегистрации = ТекущаяДата();
    МенеджерЗаписи.Записать(Истина); // Замещаем запись, если она уже была

КонецПроцедуры

А теперь посмотрим на логику регламентного задания, которое обрабатывает эту очередь пакетами в отдельных транзакциях:


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

Решение 4. Использование ДополнительныхСвойств и обработчика ПослеЗаписи

Если обновление должно происходить строго синхронно в сеансе пользователя, но вы не хотите перегружать и подвергать рискам основную транзакцию проведения, можно использовать механизм переноса логики в обработчик ПослеЗаписи.

Во время проведения документа мы лишь фиксируем намерение обновить связанные данные, помещая флаг в коллекцию ДополнительныеСвойства:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // Основная логика проведения платежа...
    
    // Регистрируем флаг намерения обновить связанные документы
    ЭтотОбъект.ДополнительныеСвойства.Вставить("ТребуетсяОбновитьСвязанныеДокументы", Истина);

КонецПроцедуры

В модуле формы документа или в подписке на событие ПослеЗаписи на сервере, когда основная транзакция проведения уже гарантированно зафиксирована в базе данных и все блокировки сняты, мы выполняем обновление связанных объектов в новой изолированной транзакции:


Процедура ПослеЗаписи(ПараметрыЗаписи)
    
    Если ЭтотОбъект.ДополнительныеСвойства.Свойство("ТребуетсяОбновитьСвязанныеДокументы") Тогда
        
        // Запускаем обновление связанных данных в новой транзакции
        НачатьТранзакцию();
        Попытка
            // Процедура обновления связанных документов
            ОбновитьСвязанныеДокументы(); 
            
            ЗафиксироватьТранзакцию();
        Исключение
            ОтменитьТранзакцию();
            // Выводим предупреждение пользователю. 
            // При этом платежка уже успешно проведена, ее проведение не откатится!
            ПоказатьПредупреждение(, "Платеж проведен успешно, но связанные заказы не были обновлены автоматически. Пожалуйста, проверьте их вручную.");
        КонецПопытки;
        
    КонецЕсли;
    
КонецПроцедуры

Резюмируем правила работы с транзакциями

При проектировании систем автоматизации в 1С всегда помните три ключевых правила:

← На главную