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