Как правильно синхронизировать реквизиты между связанными документами в 1С и какое событие подписки выбрать?

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

В практике разработки на платформе «1С:Предприятие 8» часто возникает задача синхронизации данных между различными объектами. Типичный пример: в информационной базе есть документ РеализацияТоваровУслуг с реквизитом ТипБСО, и есть документ ЗаданиеНаПеревозку, в табличной части которого содержатся ссылки на эти реализации, а также дублируется реквизит ТипБСО. Перед разработчиком встает задача: при изменении реквизита в одном документе автоматически обновить его в связанном документе, и наоборот.

В данной статье мы подробно разберем, как решить эту задачу с помощью механизмов подписок на события, проанализируем разницу между событиями ПередЗаписью и ПриЗаписи, научимся избегать критической ошибки бесконечной рекурсии (зацикливания записи), а также рассмотрим, почему дублирование реквизитов — это архитектурный антипаттерн, и как сделать правильно без лишнего кода синхронизации.

Выбор события для подписки: ПередЗаписью или ПриЗаписи?

Для реализации синхронизации нам необходимо понять, в какой момент жизненного цикла объекта базы данных выполнять программный код. Платформа предоставляет нам два основных события при записи объекта: ПередЗаписью и ПриЗаписи. Рассмотрим их особенности и применимость для нашей задачи.

Событие ПередЗаписью (BeforeWrite):

  1. Назначение: Используется для анализа и модификации реквизитов самого записываемого объекта, проверок корректности данных и возможности отмены записи (параметр Отказ). В этот момент транзакция записи уже открыта, но данные в физические таблицы базы данных еще не записаны.
  2. Доступ к старым значениям: Это единственный момент, когда мы можем сравнить новые значения реквизитов, находящиеся в оперативной памяти (в объекте Источник), со старыми значениями, которые все еще сохранены в базе данных.
  3. Ограничение: Крайне не рекомендуется в этом событии записывать другие связанные объекты базы данных. Если в процессе записи текущего объекта произойдет ошибка или сработает отказ, изменения в связанных объектах могут привести к блокировкам СУБД или нарушению целостности данных, если транзакция не откатится корректно.

Событие ПриЗаписи (OnWrite):

  1. Назначение: Используется для совершения сопутствующих действий в базе данных — записи движений по регистрам, создания или изменения связанных документов и справочников. В этот момент сам объект уже физически записан в базу данных (хотя транзакция еще не зафиксирована окончательно).
  2. Ограничение: В этом событии невозможно узнать, какие реквизиты объекта были изменены в процессе текущей интерактивной правки, поскольку в базе данных уже лежат новые, перезаписанные значения. Сравнение «было/стало» выполнить не удастся.

Правильный паттерн проектирования подписки:

Для надежной синхронизации данных мы должны использовать связку двух этих событий через специальное транзитное хранилище параметров объекта — свойство ДополнительныеСвойства:

  1. В подписке на событие ПередЗаписью мы выполняем проверку: изменился ли интересующий нас реквизит. Если изменился, мы фиксируем этот факт в структуре дополнительных свойств объекта.
  2. В подписке на событие ПриЗаписи мы проверяем наличие флага изменения в дополнительных свойствах и, если он установлен, выполняем программное изменение и запись связанных документов.

Как технически определить изменение реквизита?

Выясним, как правильно проверить, изменилось ли значение реквизита. Начинающие разработчики часто совершают грубую ошибку производительности, обращаясь к старым данным через точку от ссылки:


// НЕПРАВИЛЬНО! Ошибка производительности!
Если Источник.ТипБСО <> Источник.Ссылка.ТипБСО Тогда
    // Код обработки
КонецЕсли;

При обращении к конструкции Источник.Ссылка.Реквизит платформа 1С неявно считывает из базы данных абсолютно весь объект со всеми его реквизитами и табличными частями. Если объект «тяжелый» (например, реализация на 1000 строк), это приведет к колоссальным потерям производительности и избыточной нагрузке на СУБД.

Рассмотрим правильный и быстрый способ определения изменений с помощью точечного SQL-запроса в подписке ПередЗаписью:


Процедура ОпределитьИзменениеРеквизитаПередЗаписью(Источник, Отказ, РежимЗаписи, РежимПроведения) Экспорт

    // Если объект новый, то предыдущего значения в БД нет, и сверка не требуется
    Если Источник.ЭтоНовый() Тогда
        Возврат;
    КонецЕсли;

    // Выполняем легкий запрос к СУБД для получения только одного конкретного поля
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Реализация.ТипБСО КАК ТипБСО
        |ИЗ
        |	Документ.РеализацияТоваровУслуг КАК Реализация
        |ГДЕ
        |	Реализация.Ссылка = &Ссылка";
    
    Запрос.УстановитьПараметр("Ссылка", Источник.Ссылка);
    РезультатЗапроса = Запрос.Выполнить();
    
    Если НЕ РезультатЗапроса.Пустой() Тогда
        Выборка = РезультатЗапроса.Выбрать();
        Выборка.Следующий();
        
        // Сравниваем значение в памяти со значением в базе данных
        Если Источник.ТипБСО <> Выборка.ТипБСО Тогда
            // Реквизит изменился. Запоминаем это в ДополнительныеСвойства
            Источник.ДополнительныеСвойства.Вставить("ТипБСО_Изменен", Истина);
            Источник.ДополнительныеСвойства.Вставить("СтароеЗначениеТипБСО", Выборка.ТипБСО);
        КонецЕсли;
    КонецЕсли;

КонецПроцедуры

Опасность зацикливания при взаимной синхронизации

Проанализируем опасную ловушку, в которую попадают разработчики при попытке настроить двустороннюю синхронизацию. Сценарий выглядит так:

  1. Пользователь меняет реквизит в документе Реализация.
  2. Срабатывает подписка на запись Реализации, которая находит связанный документ ЗаданиеНаПеревозку, меняет в нем реквизит и вызывает метод Записать().
  3. Запись документа ЗаданиеНаПеревозку активирует его собственную подписку на синхронизацию. Эта подписка находит нашу Реализацию, меняет в ней реквизит и вызывает метод Записать().
  4. Снова срабатывает подписка Реализации...

В результате система уходит в бесконечный цикл перезаписи объектов, который завершится аварийным падением платформы по причине переполнения стека (Stack Overflow) или превышения времени ожидания блокировок СУБД.

Как избежать зацикливания транзакций?

Для предотвращения подобных ситуаций необходимо использовать встроенный механизм платформы — свойство объекта ОбменДанными.Загрузка. Перед программной записью связанного документа в коде подписки мы должны принудительно устанавливать этот флаг в значение Истина. Это скажет платформе и другим подпискам, что объект записывается служебно, и выполнять для него стандартные обработчики бизнес-логики не нужно.

Рассмотрим пример реализации безопасной перезаписи в подписке ПриЗаписи:


Процедура СинхронизироватьПриЗаписи(Источник, Отказ) Экспорт

    // Если запись инициирована служебным процессом обмена или другой подпиской, прерываем выполнение
    Если Источник.ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;

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

КонецПроцедуры

Архитектурная критика: почему дублирование данных — это плохо?

Несмотря на то, что технически задачу взаимной синхронизации решить можно, сама постановка этой задачи содержит грубую архитектурную ошибку — нарушение нормализации базы данных через дублирование информации.

Рассмотрим классический пример, который привели участники обсуждения на форуме. Представим, что у вас есть справочник Сотрудники. Если сотрудник меняет фамилию, вы же не будете программно открывать все проведенные документы за прошлые годы, в которых фигурирует этот сотрудник, чтобы перезаписать в текстовых полях его новую фамилию? Разумеется, нет. Фамилия хранится в одном месте (в справочнике), а во всех документах мы храним лишь ссылку на этот элемент справочника.

Дублирование реквизита ТипБСО в документе ЗаданиеНаПеревозку создает колоссальные проблемы:

  1. Раздувание базы данных и усложнение кода: Вместо лаконичной конфигурации мы вынуждены писать сложные подписки на события, контролировать транзакции, обрабатывать ошибки блокировок и следить за тем, чтобы данные не рассинхронизировались.
  2. Проблема закрытых периодов: Если пользователь решит изменить ТипБСО в документе Реализация годичной давности, ваша подписка попытается записать документ ЗаданиеНаПеревозку, который также находится в закрытом для редактирования периоде. Это приведет к ошибкам прав доступа или нарушению целостности последовательностей учета.

Правильное архитектурное решение без дублирования

Выясним, как спроектировать систему правильно, красиво и без написания громоздких подписок на события. Достаточно придерживаться следующего правила: каждая сущность должна храниться ровно в одном месте.

В нашем сценарии наиболее логичным владельцем реквизита ТипБСО является документ Реализация. Соответственно:

  1. Из табличной части документа ЗаданиеНаПеревозку необходимо полностью удалить реквизит (колонку) ТипБСО. Хранить его там физически не нужно.
  2. Везде, где требуется видеть этот реквизит (на формах, в отчетах, в печатных формах), мы будем получать его динамически по ссылке на Реализацию.

1. Динамический вывод на управляемую форму (без программирования):

Если вам нужно, чтобы пользователь в табличной части документа ЗаданиеНаПеревозку видел колонку ТипБСО, платформа 1С на управляемых формах позволяет вывести ее автоматически через механизм «пути к данным»:

Платформа «1С:Предприятие» самостоятельно на лету будет считывать актуальное значение ТипБСО непосредственно из соответствующего документа Реализация и отображать его в таблице. При этом никакого дублирования в базе данных не происходит, а пользователь всегда видит 100% актуальные данные!

2. Получение данных в печатных формах и отчетах:

Если вам нужно вывести ТипБСО в печатную форму документа ЗаданиеНаПеревозку или использовать его в отчете, вы просто получаете его в запросе через левое соединение (или через обращение через точку в запросе):


// Пример запроса для печати Задания на перевозку
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЗаданиеТЧ.НомерСтроки КАК НомерСтроки,
    |	ЗаданиеТЧ.Реализация КАК Реализация,
    |	ЗаданиеТЧ.Реализация.ТипБСО КАК ТипБСО // Получаем данные напрямую из связанного объекта
    |ИЗ
    |	Документ.ЗаданиеНаПеревозку.Реализации КАК ЗаданиеТЧ
    |ГДЕ
    |	ЗаданиеТЧ.Ссылка = &Ссылка";

Этот подход гарантирует высокую производительность, отсутствие взаимных блокировок СУБД, исключает необходимость поддержки сложного кода синхронизации и избавляет от проблем с закрытыми периодами.

Исключение из правил: Дублирование реквизита в подчиненном документе оправдано только в одном случае — если это исторические данные. То есть, если значение ТипБСО в Задании на перевозку должно «заморозиться» на момент оперативного планирования рейса и не должно меняться в будущем, даже если кто-то задним числом изменит саму Реализацию (аналогично ценам и скидкам в заказах клиентов). Но если вам требуется постоянная двусторонняя актуальность и синхронизация — динамическое получение данных через ссылку является единственно верным архитектурным решением.

← На главную