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