Почему при записи элемента справочника событие «ПередЗаписью» срабатывает дважды?

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

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

Анализ ситуации: влияние интерфейса на программный код

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

Проанализируем причину такого поведения. В обычном приложении 1С взаимодействие интерфейса и серверной части тесно связано. Когда вы нажимаете кнопку «ОК» или «Записать», платформа инициирует цепочку событий. Если код выполняется мгновенно, может возникнуть ситуация «гонки» или повторного вызова события формой из-за изменения состояния объекта. Использование точки останова или модального окна Сообщить() принудительно прерывает поток выполнения, дает интерфейсу время обновить свое состояние (например, сбросить флаг Модифицированность), и повторный вызов не происходит.

Почему нельзя писать бизнес-логику в модуле формы

Одной из главных ошибок, представленных в примере, является размещение кода создания связанных объектов в модуле формы. Рассмотрим, почему это критично:

  1. Отсутствие транзакционности: Событие ПередЗаписью в модуле формы происходит еще до начала транзакции базы данных. Если создание уведомления прошло успешно, а сама «Задача» не записалась (например, из-за ошибки в модуле объекта или нарушения контроля уникальности), в базе останется «мусорное» уведомление.
  2. Зависимость от интерфейса: Код обращается напрямую к ЭлементыФормы. Если запись этого справочника будет инициирована программно из другой обработки (не через форму), уведомление создано не будет, либо код вызовет ошибку.
  3. Двойные вызовы: Форма может инициировать запись несколько раз в процессе своей жизнедеятельности (например, при проверке заполнения или при закрытии).

Решение 1: Перенос логики в модуль объекта и работа в транзакции

Для обеспечения целостности данных мы должны перенести создание уведомления в модуль объекта. Выясним причину, почему это лучше: код в модуле объекта выполняется внутри транзакции записи. Если что-то пойдет не так, вся транзакция откатится, и лишних записей не появится.

Рассмотрим пример правильного кода в модуле объекта справочника «Задачи»:


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

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

Решение 2: Использование коллекции ДополнительныеСвойства

Чтобы форма «сообщила» объекту о необходимости создать уведомление, мы используем коллекцию ДополнительныеСвойства. Это механизм, позволяющий передавать данные из формы или внешней обработки в модуль объекта «транзитом», не сохраняя их в реквизитах базы данных.

В модуле формы это будет выглядеть так:


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

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

Решение 3: Проверка на существование записи (идемпотентность)

Хорошим тоном в программировании 1С считается проверка на существование создаваемой записи перед выполнением метода Записать(). Это защищает от «говнокода», когда при каждом нажатии кнопки создаются новые и новые элементы.

Проанализируем ситуацию: если уведомление привязано к конкретному событию или коду задачи, мы можем выполнить запрос перед созданием нового элемента:


Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 Ссылка ИЗ Справочник.УведомленияПользователей 
               ГДЕ КодИнициатора = &КодИнициатора И Дата >= &НачалоДня";
Запрос.УстановитьПараметр("КодИнициатора", КодЗадачи);
Запрос.УстановитьПараметр("НачалоДня", НачалоДня(ТекущаяДата()));

Если Запрос.Выполнить().Пустой() Тогда
    // Только если уведомление еще не создавалось сегодня, создаем новое
    // ... код создания элемента ...
КонецЕсли;

Как использовать стек вызовов для диагностики

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

  1. Установите точку останова в процедуре ПередЗаписью.
  2. Когда выполнение остановится в первый раз, нажмите F5.
  3. При втором «заходе» в процедуру откройте окно «Стек вызовов» (Call Stack) в конфигураторе (меню Отладка -> Стек вызовов).
  4. Проанализируйте, какая системная процедура или какой кусок кода вызвал повторную запись. Часто оказывается, что это подписки на события или автоматическое обновление связанных списков.

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

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

← На главную