Стандартный механизм версионирования объектов в 1С (через БСП или платформенную "Историю данных") отлично справляется с хранением истории изменений. Однако часто перед разработчиком ставится задача: сделать так, чтобы конкретные изменения (какой реквизит был изменен, какое значение было «до» и какое стало «после») отображались непосредственно в Журнале регистрации (ЖР). Это удобно для оперативного аудита без открытия специализированных отчетов.
В этой статье мы подробно разберем, как реализовать такой механизм, проанализируем различные способы сравнения данных и выясним, как правильно организовать запись в ЖР, не нарушая целостность транзакций и не снижая производительность системы.
Основная сложность заключается в том, что стандартная запись Журнала регистрации при событии "Данные.Изменение" фиксирует только сам факт записи объекта. Чтобы понять, что именно изменилось, нам необходимо:
Рассмотрим по шагам, как это реализовать наиболее эффективно.
Для того чтобы вычислить разницу, нам нужно два набора данных. Текущие данные находятся в свойствах объекта (ЭтотОбъект). Старые данные необходимо прочитать из базы данных, используя ссылку на этот же объект. Важно помнить, что если объект новый (ЭтоНовый() = Истина), то сравнивать не с чем — мы просто фиксируем создание.
Проанализируем оптимальное место для кода сравнения. Лучше всего использовать обработчик ПередЗаписью в модуле объекта или в Подписке на события. Почему не ПриЗаписи? Потому что в момент ПриЗаписи данные в базе уже могут быть обновлены (в зависимости от режима), и получить «старую» версию через Ссылка.ПолучитьОбъект() может быть затруднительно.
Вместо того чтобы вручную прописывать сравнение каждого поля (Наименование, ИНН, Склад), мы воспользуемся коллекцией метаданных. Это сделает наш код универсальным для любого справочника.
Рассмотрим пример кода, который можно разместить в общем модуле для вызова из подписки на событие ПередЗаписью:
Процедура ОпределитьРазницуРеквизитов(Источник, Отказ) Экспорт
Если Источник.ОбменДанными.Загрузка Или Источник.ЭтоНовый() Тогда
Возврат;
КонецЕсли;
// Читаем состояние объекта из базы данных напрямую
СтарыйОбъект = Источник.Ссылка.ПолучитьОбъект();
СписокИзменений = Новый СписокЗначений;
// Обходим основные реквизиты через метаданные
Для Каждого Реквизит Из Источник.Метаданные().Реквизиты Цикл
ИмяРеквизита = Реквизит.Имя;
ЗначениеНовое = Источник[ИмяРеквизита];
ЗначениеСтарое = СтарыйОбъект[ИмяРеквизита];
Если ЗначениеНовое <> ЗначениеСтарое Тогда
ТекстИзменения = СтрШаблон("Изменен реквизит '%1': Было '%2', Стало '%3'",
Реквизит.Синоним, ЗначениеСтарое, ЗначениеНовое);
СписокИзменений.Добавить(ТекстИзменения);
КонецЕсли;
КонецЦикла;
// Сохраняем результат для последующей записи в ЖР
Если СписокИзменений.Количество() > 0 Тогда
Источник.ДополнительныеСвойства.Вставить("СписокИзмененийДляЖР", СписокИзменений);
КонецЕсли;
КонецПроцедуры
В этом примере мы используем ДополнительныеСвойства объекта. Это критически важный момент: это хранилище «живет» в течение всей транзакции. Мы записываем туда изменения ПередЗаписью, но в сам Журнал регистрации отправим их только ПослеЗаписи.
Почему нельзя писать в ЖР сразу в ПередЗаписью? Если после выполнения этого кода система выдаст ошибку (например, сработает проверка заполнения или произойдет сбой СУБД), запись в ЖР останется, а данные в базе не изменятся. Это приведет к дезинформации пользователей.
Поэтому используем обработчик ПослеЗаписи:
Процедура ЗаписатьИзмененияВЖурнал(Источник, ПараметрыЗаписи) Экспорт
Если Источник.ДополнительныеСвойства.Свойство("СписокИзмененийДляЖР") Тогда
СписокИзменений = Источник.ДополнительныеСвойства.СписокИзмененийДляЖР;
ТекстСообщения = "";
Для Каждого ЭлементСписка Из СписокИзменений Цикл
ТекстСообщения = ТекстСообщения + ЭлементСписка.Значение + Символы.ПС;
КонецЦикла;
ЗаписьЖурналаРегистрации(
"Изменение реквизитов справочника",
УровеньЖурналаРегистрации.Информация,
Источник.Метаданные(),
Источник.Ссылка,
ТекстСообщения
);
КонецЕсли;
КонецПроцедуры
Сравнение табличных частей (ТЧ) — более трудоемкий процесс. Простого цикла по метаданным недостаточно, так как строки могут добавляться, удаляться или меняться местами. Проанализируем один из подходов: использование временных таблиц.
Выясним причину сложности: в ТЧ нет уникальных идентификаторов строк, которые сохраняются между сессиями. Поэтому обычно используют следующий алгоритм:
Для более продвинутых систем рекомендуется записывать изменения не в виде простого текста, а в формате JSON. Рассмотрим, чем это полезно. Если в будущем вам потребуется построить отчет «Кто и когда менял поле Цена», программно прочитать JSON из комментария ЖР гораздо проще, чем парсить обычный текст.
// Пример формирования структуры для JSON
ИзмененияСтруктура = Новый Структура;
ИзмененияСтруктура.Вставить("ИмяПоля", "Склад");
ИзмененияСтруктура.Вставить("СтароеЗначение", Строка(ЗначениеСтарое));
ИзмененияСтруктура.Вставить("НовоеЗначение", Строка(ЗначениеНовое));
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ИзмененияСтруктура);
РезультатСтрокой = ЗаписьJSON.Закрыть();
Необходимо понимать, что Журнал регистрации не предназначен для хранения больших объемов данных. Проанализируем потенциальные проблемы:
.lgp или .lgf до десятков гигабайт.Ссылка.ПолучитьОбъект() выполняет полноценный запрос к СУБД. При массовой записи это может существенно замедлить работу.Если ваша конфигурация работает на платформе 8.3.11 и выше, рассмотрите возможность включения стандартного свойства метаданных История данных. Она работает на уровне платформы и хранит изменения в специальных таблицах БД, что гораздо производительнее Журнала регистрации. Получить разницу между версиями из Истории данных можно программно с помощью менеджера ИсторияДанных, и уже эти готовые данные (разницу) при необходимости транслировать в ЖР.
Подведем итог: использование связки ПередЗаписью (для анализа), ДополнительныеСвойства (для передачи данных) и ЗаписьЖурналаРегистрации в событии ПослеЗаписи является наиболее надежным способом реализации ТЗ по детальному логированию в Журнал регистрации.