Определение проблемы
Есть группа взаимосвязанных документов, содержащих зависимые реквизиты. При изменении одного документа необходимо отследить зависимости и обновить зависимые реквизиты в связанных документах.
Логика отслеживания зависимостей инкапсулирована в модуле каждого документа. Упрощенно, в модулях документов определена экспортная процедура:
Процедура ОбновитьЗависимыеРеквизиты(Знач Источник) Экспорт
ОбновитьЗависимыеРеквизитыТекущегоДокумента(Источник);
Записать(РежимЗаписиДокумента.Проведение);
ЗависимыеДокументы = ПолучитьЗависимыеДокументы();
Для каждого Ссылка Из ЗависимыеДокументы Цикл
ДокОбъект = Ссылка.ПолучитьОбъект();
ДокОбъект.ОбновитьЗависимыеРеквизиты(ЭтотОбъект);
КонецЦикла;
КонецПроцедуры
Все документы группы должны быть изменены согласованно, поэтому инициирующая сторона должна запустить изменение в транзакции:
НачатьТранзакцию();
Попытка
// ...здесь нужно заблокировать всю группу, но как?
КорневойДокумент.ОбновитьЗависимыеРеквизиты(Источник);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Возникающая при этом проблема озвучена в комментарии к вышеприведенному коду.
Если мы изменяем группу объектов в транзакции, то при параллельной работе возникает риск взаимной блокировки (deadlock). Чтобы его исключить, мы должны наложить управляемые блокировки на все изменяемые объекты до того, как начнем записывать первый из них. Но как инициирующая сторона сможет определить множество блокируемых документов, если логика определения этого множества инкапсулирована?
Решение "в лоб" может выглядеть следующим образом: в модуле объекта определить функцию, которая вернет массив ссылок на документы. Инициирующая сторона может использовать этот массив для блокировки всех документов перед началом их обновления.
НачатьТранзакцию();
Попытка
ДокументыГруппы = КорневойДокумент.ВсеДокументыГруппы();
Блокировка = Новый БлокировкаДанных;
Для каждого Ссылка Из ДокументыГруппы Цикл
//...добавить блокировку по ссылке
КонецЦикла;
Блокировка.Заблокировать();
КорневойДокумент.ОбновитьЗависимыеРеквизиты(Источник);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Очевидный недостаток этого решения - выполнение в два прохода. Первый раз придется пройтись по графу документов для определения множества блокируемых объектов, и второй раз - для обновления данных.
Паттерн "Единица работы"
В книге Мартина Фаулера "Шаблоны корпоративных приложений" описан паттерн "Единица работы" (Unit of Work).
Суть в следующем: производя серию изменений взаимосвязанных объектов, мы не записываем каждый объект в БД сразу после его изменения в памяти, а регистрируем операцию изменения в специальном журнале. В самом конце мы выполняем транзакционную запись всех измененных объектов в БД, предварительно накладывая все необходимые блокировки.
Порядок действий таков:
- Выполнить изменения объектов в памяти, при этом внося измененные объекты в журнал.
- После выполнения всех требуемых изменений - начать транзакцию.
- Заблокировать каждый объект журнала.
- Выполнить операцию с каждым объектом журнала (записать, удалить, пометить на удаление).
- Зафиксировать транзакцию.
В объектной технике исполнения журнал и все методы работы с ним реализуются в виде объекта, который можно легко передавать по иерархии вложенных вызовов. Таким образом сохраняется инкапсуляция внутренней логики изменяемых объектов: сторона, инициирующая изменение, не обязана ничего знать о природе взаимосвязей внутри группы. Изменяемые объекты сами регистрируют операции изменения в журнале, инициирующей стороне остается только зафиксировать изменения в самом конце.
Реализация
Термин "Единица работы" мне показался не очень внятным, поэтому я дал своему объекту более понятное имя "Пакет изменений". Он представляет из себя обработку, реализация которой приведена под спойлером.
Данная реализация поддерживает работу со всеми объектами ссылочного типа.
Основные методы:
- Записать(Объект, Параметр1, Параметр2) - добавить операцию записи объекта. Два последних параметра должны использоваться только для документов.
- УстановитьПометкуУдаления(Объект, Пометка, ВключаяПодчиненные) - добавить операцию установки пометки удаления. Последний параметр должен использоваться только для иерархических типов.
- Удалить(Объект) - добавить операцию удаления объекта.
- ВыполнитьВТранзакции() - применить все блокировки и выполнить все операции в транзакции.
По умолчанию ПакетИзменений использует режим автоматической блокировки объектов. Не следует путать с термином "автоматические блокировки" платформы 1С. В данном случае это его собственный режим, который означает, что для каждого добавляемого в журнал объекта будет также автоматически добавляться элемент в пакетную управляемую блокировку. Но само блокирование при этом не выполняется - оно будет выполнено только при вызове метода ВыполнитьВТранзакции.
Если требуется более тонкое управление блокировками, то для этого предусмотрена группа дополнительных методов:
- АвтоматическаяБлокировка(Состояние) - Позволяет включить/выключить режим автоматической блокировки, а также получить его текущее состояние.
- ДобавитьБлокировкуПоСсылке(Ссылка, Режим) - Добавляет элемент пакетной блокировки для блокирования объекта по ссылке.
- ДобавитьБлокировку(ПространствоБлокировки) - Создает и возвращает новый элемент пакетной управляемой блокировки - для добавления произвольных блокировок.
- ЗаблокироватьОбъекты() - Применяет пакетную блокировку со всеми ранее добавленными элементами блокировки.
- ВыполнитьОперации() - Выполняет все операции с объектами, ранее добавленные в журнал.
При использовании последних двух методов вызывающая сторона должна сама позаботиться об открытии и фиксации транзакции.
Использование
С обработкой ПакетИзменений задача решается следующим образом.
Процедура обновления в модуле объекта теперь может выглядеть так:
Процедура ОбновитьЗависимыеРеквизиты(Знач Источник, Знач ПакетИзменений) Экспорт
ОбновитьЗависимыеРеквизитыТекущегоДокумента(Источник);
// вместо записи объекта добавляем операцию в журнал
ПакетИзменений.Записать(ЭтотОбъект, РежимЗаписиДокумента.Проведение);
ЗависимыеДокументы = ПолучитьЗависимыеДокументы();
Для каждого Ссылка Из ЗависимыеДокументы Цикл
ДокОбъект = Ссылка.ПолучитьОбъект();
ДокОбъект.ОбновитьЗависимыеРеквизиты(ЭтотОбъект, ПакетИзменений); // передаем пакет дальше
КонецЦикла;
КонецПроцедуры
Инициирующая сторона:
ПакетИзменений = Обработки.ПакетИзменений.Создать();
КорневойДокумент.ОбновитьЗависимыеРеквизиты(Источник, ПакетИзменений);
// здесь происходит реальная запись в БД
ПакетИзменений.ВыполнитьВТранзакции();
Ограничения
Ну как же без ложки дёгтя.
Данный метод не годится, если логика обновления требует выполнения согласованного чтения всех объектов группы. В этом случае необходимо выполнить блокирование всех объектов группы еще до начала чтения их из БД. Однако, с учетом того, что для определения связей все равно приходится сначала прочитать документы из БД, требование согласованного чтения делает задачу очень нетривиальной. Вплоть до того, что хранение графа связей придется выносить в отдельный регистр.
Текущая реализация не работает с наборами записей регистров сведений. Также не поддерживаются операции БизнесПроцессОбъект.Старт() и ЗадачаОбъект.ВыполнитьЗадачу(). При необходимости, это все нетрудно добавить.
Также нет никакой оптимизации в отношении возможного повторного добавления объектов в журнал - дважды добавленный объект будет действительно записан дважды.
Вступайте в нашу телеграмм-группу Инфостарт