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