В процессе разработки на платформе 1С:Предприятие 8.3 часто возникает задача отслеживания изменений в объектах. Это может быть необходимо для ведения собственного лога аудита, реализации сложной бизнес-логики, которая зависит от того, изменился ли конкретный реквизит, или для запрета редактирования уже заполненных данных. В данной статье мы подробно разберем, как эффективно сравнивать старые и новые значения реквизитов непосредственно в модуле объекта или в подписках на события.
Когда мы работаем в обработчике ПередЗаписью модуля объекта, у нас в оперативной памяти уже находятся измененные данные (в свойствах ЭтотОбъект), но в самой базе данных (на жестком диске) все еще хранятся старые значения. Ключом к получению этих «старых» значений является свойство Ссылка.
Разберем базовый принцип: если мы обратимся к реквизиту напрямую (например, Наименование), мы получим то значение, которое пользователь только что ввел на форме. Если же мы обратимся через ссылку (Ссылка.Наименование), платформа выполнит запрос к базе данных и вернет то значение, которое было сохранено до начала текущей транзакции записи.
Рассмотрим простейший пример кода в модуле объекта:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ЗначениеЗаполнено(Ссылка) Тогда
// Сравниваем конкретный реквизит
Если ЭтотОбъект.Статус <> Ссылка.Статус Тогда
Сообщить("Статус изменился с " + Ссылка.Статус + " на " + ЭтотОбъект.Статус);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Важно помнить, что проверка ЗначениеЗаполнено(Ссылка) необходима, так как при создании нового объекта ссылка еще пустая, и попытка обращения к ее реквизитам не имеет смысла (там будут значения по умолчанию).
Если нам нужно отследить изменения не в одном конкретном поле, а во всем справочнике сразу, писать условия для каждого реквизита вручную — неэффективно. Проанализируем, как можно автоматизировать этот процесс с помощью коллекции метаданных. Мы можем перебрать все реквизиты, определенные для данного объекта, и сравнить их значения динамически.
Рассмотрим пример универсального цикла:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Проверяем, что это не новый объект
Если ЭтоНовый() Тогда
Возврат;
КонецЕсли;
// Получаем метаданные текущего объекта
МетаданныеОбъекта = ЭтотОбъект.Метаданные();
// Перебираем все реквизиты, указанные в конфигураторе
Для Каждого МДРеквизит Из МетаданныеОбъекта.Реквизиты Цикл
ИмяРеквизита = МДРеквизит.Имя;
СтароеЗначение = Ссылка[ИмяРеквизита];
НовоеЗначение = ЭтотОбъект[ИмяРеквизита];
Если СтароеЗначение <> НовоеЗначение Тогда
// Здесь фиксируем изменение
ЗафиксироватьИзменение(ИмяРеквизита, СтароеЗначение, НовоеЗначение);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Обратите внимание на использование синтаксиса квадратных скобок ЭтотОбъект[ИмяРеквизита]. Это позволяет обращаться к свойствам объекта по строковому имени, что делает код гибким и независимым от конкретного набора полей справочника.
При использовании подхода Ссылка.ИмяРеквизита внутри цикла, нужно учитывать важный нюанс производительности. Платформа 1С при каждом таком обращении может инициировать отдельное обращение к базе данных, если объект «тяжелый» или если вы обращаетесь к большому количеству полей. Это существенно замедляет запись, особенно при массовых обработках.
Выясним, как оптимизировать этот процесс. Если реквизитов много, правильнее будет выполнить один запрос к базе данных, прочитать все старые значения в структуру или таблицу значений, а затем проводить сравнение в оперативной памяти.
Разберем пример с использованием запроса для получения старых данных:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ * ИЗ Справочник.Контрагенты ГДЕ Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Для Каждого МДРеквизит Из Метаданные.Справочники.Контрагенты.Реквизиты Цикл
Имя = МДРеквизит.Имя;
Если ЭтотОбъект[Имя] <> Выборка[Имя] Тогда
// Логика обработки изменений
КонецЕсли;
КонецЦикла;
КонецЕсли;
Этот метод гарантирует, что мы обращаемся к базе данных ровно один раз, независимо от количества проверяемых реквизитов.
Часто задача состоит в том, чтобы не просто найти изменения, а передать информацию о них на следующий этап — например, в процедуру ПриЗаписи, где уже можно безопасно фиксировать данные в сторонних регистрах. Для этих целей идеально подходит свойство ДополнительныеСвойства.
Проанализируем алгоритм работы:
ПередЗаписью проводим сравнение.ЭтотОбъект.ДополнительныеСвойства.Вставить("СписокИзменений", ТаблицаИзменений).ПриЗаписи извлекаем эти данные и записываем в Регистр Сведений.Почему это важно? Обработчик ПриЗаписи выполняется уже после того, как данные основного объекта были успешно помещены в базу, но до фиксации (commit) транзакции. Если запись лога в регистр завершится ошибкой, вся транзакция (включая запись самого справочника) будет отменена. Это обеспечивает целостность данных.
При реализации логики сравнения важно не забывать о системных параметрах, которые могут повлиять на работу кода. Выясним основные моменты:
ОбменДанными.Загрузка установлено в Истина, это означает, что данные записываются из обмена или внешней обработки. В большинстве случаев в этом режиме сложную логику сравнения и ведения логов стоит отключать, чтобы избежать конфликтов и замедления импорта.ПараметрыСеанса.ТекущийПользователь, предварительно убедившись, что параметры сеанса инициализированы.ЭтотОбъект.Товары = Ссылка.Товары не сработает. Потребуется выгрузить обе коллекции в таблицы значений и сравнить их построчно или с помощью запроса.Рассмотрим ситуацию, когда в конфигурации уже подключена Библиотека Стандартных Подсистем (БСП). В ней есть мощный механизм «Версионирование объектов». Прежде чем писать свой велосипед, проанализируем его возможности:
Однако, если ваша цель — создать максимально легкий и быстрый механизм, реагирующий только на изменение заполненных реквизитов (как просил автор темы), то ручное сравнение в модуле объекта с записью в специализированный регистр сведений будет более производительным решением для высоконагруженных систем.
Для реализации качественного механизма сравнения реквизитов придерживайтесь следующего алгоритма:
ЭтоНовый(), чтобы не сравнивать пустые ссылки.Метаданные() для создания универсального кода, который не сломается при добавлении новых реквизитов в конфигураторе.Ссылка.ПередЗаписью в ПриЗаписи через ДополнительныеСвойства для обеспечения транзакционной безопасности.ОбменДанными.Загрузка, чтобы не блокировать интеграционные процессы.