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