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