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