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