Как определить новый элемент справочника в подписке на событие «При записи» в 1С

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

При разработке универсальных механизмов логирования, интеграции или версионирования в системе 1С часто возникает задача: отловить создание новых элементов справочника и записать информацию о них в регистр сведений или внешний лог. Казалось бы, задача тривиальная. Но при попытке реализовать её через стандартные подписки на события мы мгновенно сталкиваемся с архитектурной дилеммой платформы 1С.

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

Архитектурное решение: Связка двух подписок и использование ДополнительныхСвойств

Наиболее надежный, производительный и соответствующий стандартам разработки (включая стандарты 1С:БСП) метод заключается в использовании двух подписок на события и специальной системной коллекции ДополнительныеСвойства. Коллекция ДополнительныеСвойства представляет собой структуру, которая создается вместе с объектом в оперативной памяти и «живет» на протяжении всей транзакции записи (от момента начала записи до завершения транзакции).

Рассмотрим этот механизм по шагам. Нам понадобятся две подписки на события для всех необходимых справочников:

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

Давайте разберем по шагам программный код, который необходимо разместить в общем модуле. Обратите внимание, что общий модуль должен быть серверным.

Шаг 1. Создаем обработчик для первой подписки на событие ПередЗаписью:


Процедура ПередЗаписью_ОпределитьНовыйЭлемент(Источник, Отказ, РежимЗаписи, РежимПроведения) Экспорт

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

    // Метод ЭтоНовый() отлично работает на данном этапе
    Если Источник.ЭтоНовый() Тогда
        // Запоминаем факт того, что объект новый, в контексте самого объекта
        Источник.ДополнительныеСвойства.Вставить("ЭтоНовыйОбъектДляРегистрации", Истина);
    КонецЕсли;

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

Шаг 2. Создаем обработчик для второй подписки на событие ПриЗаписи:


Процедура ПриЗаписи_ЗарегистрироватьНовыйЭлемент(Источник, Отказ) Экспорт

    Если Источник.ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;

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

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

Шаг 3. Реализуем вспомогательный метод записи данных в регистр сведений:


Процедура ЗафиксироватьНовыйОбъектВРегистре(СсылкаНаОбъект)

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

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

Разоблачение мифа: Свойство «Только для чтения» в ДополнительныеСвойства

Часто начинающие разработчики, заглядывая в Синтакс-Помощник платформы 1С, видят у свойства ДополнительныеСвойства пометку «Только чтение» и пугаются. Они ошибочно полагают, что в эту коллекцию нельзя ничего записывать программно.

Выясним причину этого недоразумения. Пометка «Только чтение» для коллекций в 1С означает лишь то, что вы не можете присвоить свойству новую коллекцию целиком (то есть написать Источник.ДополнительныеСвойства = Новый Структура; — система выдаст ошибку). Однако сама структура внутри этого свойства полностью доступна для изменения. Мы можем без ограничений использовать методы Вставить(), Удалить(), Очистить() и менять значения элементов по ключу. Это абсолютно легальный и стандартный инструмент платформы.

Критически важный нюанс: ОбменДанными.Загрузка

Проанализируем крайне важную деталь, которую часто упускают из виду программисты при создании универсальных подписок. Это свойство ОбменДанными.Загрузка.

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

Альтернативный вариант: Локальная переменная модуля объекта

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

Посмотрим на пример реализации такого подхода в модуле объекта справочника:


// Переменная живет в памяти до окончания работы с экземпляром объекта справочника
Перем ЭтоНовыйОбъект;

Процедура ПередЗаписью(Отказ)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Фиксируем статус новизны в локальную переменную модуля
    ЭтоНовыйОбъект = ЭтоНовый();
    
КонецПроцедуры

Процедура ПриЗаписи(Отказ)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    Если ЭтоНовыйОбъект Тогда
        // Выполняем логику для нового объекта
        РегистрацияНовогоЛокально();
    КонецЕсли;
    
КонецПроцедуры

Преимущество метода: Более высокая производительность за счет отсутствия необходимости инициализации коллекций типа «Структура».

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

Почему не стоит использовать метод УстановитьСсылкуНового()

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

Таким образом, связка подписок на события ПередЗаписью и ПриЗаписи с передачей признака через ДополнительныеСвойства остается самым безопасным, чистым и надежным решением для платформы 1С.

← На главную