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