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