Как отследить изменение ресурса при записи в регистр сведений и обновить дату изменения

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

В практике разработки на платформе 1С:Предприятие 8.3 часто возникает задача: необходимо фиксировать момент последнего изменения записи в регистре сведений. Казалось бы, задача тривиальная — достаточно сравнить старое значение ресурса с новым и, если они отличаются, обновить соответствующий реквизит. Однако на практике разработчики сталкиваются с особенностями работы платформы, такими как замещение набора записей и многократный вызов обработчиков событий. В этой статье мы подробно разберем, как правильно реализовать этот механизм, избежав типичных ошибок.

Почему стандартный подход "в лоб" не работает

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

Рассмотрим основные сложности:

  1. Режим замещения: По умолчанию запись набора происходит с удалением старых записей, попадающих в отбор.
  2. Интерактивная правка: При использовании формы записи или инструмента универсальный редактор данных платформа выполняет специфическую последовательность действий, которая может привести к потере "старого" состояния для сравнения.
  3. Отсутствие встроенного механизма "Old/New": В отличие от триггеров в SQL, в 1С нет автоматического доступа к старым значениям набора внутри модуля этого набора.

Решение через использование ДополнительныхСвойств

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

Разберем пример кода, который необходимо разместить в модуле набора записей регистра сведений:


Процедура ПередЗаписью(Отказ, Замещение)
    
    // Если включен режим загрузки данных, логику контроля обычно отключают
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;

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

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

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

Нюансы при работе с Менеджером записи

Выясним причину, по которой код может вести себя странно при работе с МенеджерЗаписи. Когда вы вызываете МенеджерЗаписи.Записать(), платформа неявно создает набор записей с установленным отбором по всем измерениям. При этом свойство Замещение всегда равно Истина.

Посмотрим на пример из обсуждения: если вы используете метод Прочитать() внутри менеджера записи, он заполняет поля значениями из базы. Если вы их меняете и записываете, срабатывает наш алгоритм. Но важно помнить: если запись производится программно из одного конкретного места, иногда проще и эффективнее перенести логику проверки в это место (обработку или модуль документа), чтобы не "нагружать" модуль регистра сложными проверками при каждой транзакции.

Оптимизация: Метод сравнения таблиц

Для регистров с большим объемом данных в одном наборе (например, движения по документу) перебор строк в цикле с поиском через НайтиСтроки может быть медленным. Проанализируем более производительный подход — использование метода сворачивания таблиц.

Рассмотрим алгоритм оптимизированного сравнения:

  1. Выгружаем старые данные из БД в ТаблицаСтарая.
  2. Выгружаем новые данные из ЭтотОбъект в ТаблицаНовая.
  3. Добавляем в обе таблицы служебную колонку ХешСумма или просто объединяем их в одну таблицу.
  4. В объединенной таблице создаем колонку Счетчик. Для старых записей ставим значение 1, для новых -1.
  5. Сворачиваем таблицу по всем измерениям и ресурсам, суммируя Счетчик.

Если после сворачивания по какой-то комбинации измерений и ресурсов сумма в колонке Счетчик стала равна 0, это означает, что запись не изменилась. Все строки с ненулевым счетчиком — это либо измененные, либо удаленные, либо новые данные. Этот метод позволяет избежать вложенных циклов и работает на порядок быстрее на больших объемах данных.

Использование механизма «История данных»

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

Однако, если реквизит ВремяПоследнегоИзменения критически важен для логики конфигурации (например, по нему строятся отчеты или происходит синхронизация), то описанный выше метод с ДополнительныеСвойства остается "золотым стандартом" разработки на 1С.

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

← На главную