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

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

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

В данной статье мы подробно разберем, как решить две практические задачи:

  1. Организовать запись в регистр сведений при сохранении элемента справочника, если его модуль (и саму конфигурацию) изменять нельзя.
  2. Организовать запись в регистр сведений при изменении конкретного реквизита (в нашем примере — «Наименования»), если модуль справочника менять можно, но запрещено использовать стандартное событие ПередЗаписью модуля объекта.

---

Решение задачи №1: Запись в регистр без изменения модуля справочника

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

Способ А. Использование подписок на события

Подписка на событие — это специальный объект метаданных, который позволяет «перехватить» стандартные события (такие как ПередЗаписью, ПриЗаписи, ПередУдалением и др.) для одного или нескольких объектов конфигурации и выполнить собственный алгоритм в общем модуле.

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

  1. Создание общего модуля:

    Поскольку обработчик подписки должен располагаться во внешнем по отношению к справочнику модуле, создадим новый общий модуль (например, _РаботаСоСправочникамиСобытия). Настроим его свойства в палитре свойств:

    • Флаг Глобальный должен быть выключен.
    • Флаг Сервер должен быть включен (так как запись объектов происходит на сервере).
    • Флаг Внешнее соединение (при необходимости).
  2. Создание подписки на событие:

    В ветке конфигуратора «Общие» -> «Подписки на события» добавим новую подписку:

    • Имя: ЗаписатьВРегистрПриЗаписиСправочника
    • Источник: Выбираем необходимый справочник (или тип СправочникОбъект, если подписка создается сразу для группы справочников).
    • Событие: Выбираем событие ПриЗаписи (OnWrite).
    • Обработчик: Указываем созданную процедуру в нашем общем модуле.

Важный нюанс выбора события: Почему для записи в регистр сведений лучше использовать событие ПриЗаписи, а не ПередЗаписью? Если элемент справочника создается впервые, то в момент выполнения события ПередЗаписью у него еще нет физической ссылки в базе данных. Попытка записать пустую ссылку в измерение регистра сведений приведет к ошибке или неверной аналитике. В событии ПриЗаписи объект уже физически записан в СУБД, ссылка гарантированно существует, и ее можно безопасно использовать.

Рассмотрим пример кода обработчика в общем модуле. Обратите внимание, что процедура обязательно должна быть объявлена с ключевым словом Экспорт:


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

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

Способ Б. Использование расширений конфигурации

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

Для этого добавим справочник в расширение конфигурации, перейдем в его модуль объекта и создадим обработчик с аннотацией &После:


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

---

Решение задачи №2: Отслеживание изменения реквизита без события «ПередЗаписью» модуля объекта

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

Сложность заключается в том, что в событии ПриЗаписи новые данные уже сохранены в базу данных, и простое сравнение ЭтотОбъект.Наименование и ЭтотОбъект.Ссылка.Наименование вернет одинаковые значения, так как ссылка уже указывает на обновленную запись СУБД.

Проанализируем три альтернативных подхода к решению этой задачи.

Способ А. Оптимальное сравнение через запрос в событии «ПриЗаписи» (Выбор профессионалов)

Несмотря на то, что данные уже физически изменены в СУБД, в процессе транзакции записи у нас все еще есть возможность сравнить данные «в памяти» (которые мы записываем) со старыми данными, но делать это нужно правильно.

Часто на форумах советуют обращаться к реквизитам через точку от ссылки: ЭтотОбъект.Ссылка.Наименование. Внимание: это грубая ошибка проектирования!

При обращении через точку платформа 1С неявно считывает из СУБД весь объект целиком со всеми его реквизитами, табличными частями и кэширует его в памяти. Если объект «тяжелый», это приводит к серьезной деградации производительности.

Правильным и наиболее производительным способом является использование запроса к базе данных. Однако, чтобы получить именно «старое» значение, запрос нужно выполнять до физической фиксации транзакции. Давайте реализуем этот механизм в разрешенном событии ПриЗаписи модуля объекта справочника, используя временную переменную модуля или сравнивая данные СУБД.

Однако, так как в событии ПриЗаписи СУБД уже обновила таблицы (но транзакция еще не зафиксирована окончательно), наиболее надежным и изящным способом обойти ограничение на использование ПередЗаписью модуля объекта является использование предопределенной коллекции ДополнительныеСвойства.

Способ Б. Использование коллекции «ДополнительныеСвойства» и событий формы

У любого прикладного объекта 1С существует свойство ДополнительныеСвойства (имеющее тип Структура). Это свойство «живет» на протяжении всего сеанса записи объекта в память и передается между сервером, формой и модулем объекта.

Если пользователь записывает элемент интерактивно через форму, мы можем перехватить момент до записи в модуле формы, выполнить там легкий запрос сравнения и передать флаг в модуль объекта.

Разберем по шагам реализацию этого механизма:

Шаг 1. Код в модуле формы элемента справочника:


&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
	
	// Нам нужно проверить, изменилось ли наименование по сравнению с тем, что сейчас сохранено в базе.
	// Используем запрос для получения старого значения наименования из БД.
	Если Не ТекущийОбъект.ЭтоНовый() Тогда
		Запрос = Новый Запрос;
		Запрос.Текст = 
			"ВЫБРАТЬ
			|	Номенклатура.Наименование КАК Наименование
			|ИЗ
			|	Справочник.Номенклатура КАК Номенклатура
			|ГДЕ
			|	Номенклатура.Ссылка = &Ссылка";
		
		Запрос.УстановитьПараметр("Ссылка", ТекущийОбъект.Ссылка);
		Выборка = Запрос.Выполнить().Выбрать();
		
		Если Выборка.Следующий() Тогда
			СтароеНаименование = Выборка.Наименование;
			// Если наименование изменилось, взводим флаг в дополнительных свойствах объекта
			Если СтароеНаименование <> ТекущийОбъект.Наименование Тогда
				ТекущийОбъект.ДополнительныеСвойства.Вставить("НаименованиеИзменилось", Истина);
				ТекущийОбъект.ДополнительныеСвойства.Вставить("СтароеНаименование", СтароеНаименование);
			КонецЕсли;
		КонецЕсли;
	КонецЕсли;
	
КонецПроцедуры

Шаг 2. Код в модуле объекта справочника (в разрешенном событии «ПриЗаписи»):


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

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

Способ В. Системный механизм «История данных» (версионирование)

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

Для настройки этого механизма выполните следующие действия в конфигураторе:

  1. Откройте свойства нужного справочника.
  2. Перейдите на вкладку «История данных».
  3. Установите значение свойства «Использовать историю данных» в положение Использовать.
  4. В табличной части ниже отметьте флажками те реквизиты, изменения которых вас интересуют (в нашем случае — только реквизит Наименование).

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


ВыборкаВерсий = ИсторияДанных.ВыбратьВерсии(СсылкаНаОбъект);
Пока ВыборкаВерсий.Следующий() Цикл
	// Анализируем версии объекта и при необходимости переносим данные в свой регистр сведений
КонецЦикла;

Использование встроенной «Истории данных» позволяет существенно снизить нагрузку на основную транзакцию записи объекта СУБД, исключить блокировки таблиц базы данных и упростить дальнейшее обновление вашей конфигурации 1С.

← На главную