Как определить, был ли изменен элемент справочника в 1С перед записью?

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

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

В чем проблема со свойством ЭтотОбъект.Модифицированность?

Многие разработчики сталкиваются с тем, что в процедуре ПередЗаписью модуля объекта свойство ЭтотОбъект.Модифицированность всегда возвращает значение Истина, даже если пользователь просто открыл элемент справочника и закрыл его, не внеся никаких видимых изменений. Почему так происходит?

Давайте проанализируем, как работает свойство Модифицированность в различных контекстах платформы 1С.

  1. Объект.Модифицированность() (метод объекта метаданных): Этот метод доступен на сервере и предназначен для определения того, был ли объект изменен после считывания из базы данных.

    • Если мы получаем объект по ссылке (например, Ссылка = Справочники.МойСправочник.НайтиПоКоду("0001"); Объект = Ссылка.ПолучитьОбъект();), а затем считываем его, метод Модифицированность() всегда вернет Ложь, так как объект только что был прочитан и еще не изменялся.
    • Если же мы программно изменяем реквизит (Объект.Код = "123";), то после этого Объект.Модифицированность() вернет Истина.
    • Однако, если объект заполняется из реквизита формы, как это происходит при записи из управляемой формы, объект для обработчика ПередЗаписью создается по данным формы. В этом случае система воспринимает его как "модифицированный" относительно "пустого" состояния, даже если фактических изменений полей не было. Поэтому Модифицированность() часто возвращает Истина, что вводит в заблуждение.
  2. Форма.Модифицированность (свойство управляемой формы): Это совершенно другое свойство, доступное на клиенте. Оно сигнализирует пользователю о том, что данные на форме были изменены, отображая символ * в заголовке формы. Это свойство взводится при интерактивном изменении данных или программно, а сбрасывается при успешной записи объекта или при открытии формы до события ПриОткрытии(). Разработчик может программно управлять этим свойством (устанавливать в Истина или Ложь). Важно не путать его с методом объекта Объект.Модифицированность().

Почему ЭтотОбъект.Модифицированность всегда Истина в ПередЗаписью при работе с формами?

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

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

Как правильно определить фактические изменения реквизитов элемента справочника?

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

  1. Считывание объекта из базы данных по ссылке (Ссылка.ПолучитьОбъект()).

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

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

    Посмотрим на пример:

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

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

  2. Использование запросов для считывания отдельных реквизитов.

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

    Посмотрим на пример:

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

    Преимущества: Позволяет выбирать только нужные реквизиты, что может значительно снизить объем считываемых данных и повысить производительность при работе с "тяжелыми" объектами.

    Недостатки: Сравнение табличных частей с помощью запросов становится значительно сложнее, так как требуется несколько запросов или сложные соединения.

  3. Универсальное сравнение всех реквизитов и табличных частей.

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

    Принципы реализации:

    • Получаем старую версию объекта (например, через Ссылка.ПолучитьОбъект()).
    • Итерируем по коллекции ЭтотОбъект.Метаданные().Реквизиты.
    • Для каждого реквизита сравниваем ЭтотОбъект[ИмяРеквизита] со СтарыйОбъект[ИмяРеквизита].
    • Итерируем по коллекции ЭтотОбъект.Метаданные().ТабличныеЧасти.
    • Для каждой табличной части сравниваем ее строки: количество, затем поэлементно каждую строку по всем ее реквизитам. Это требует аккуратного определения "ключевых" полей для строк табличных частей, чтобы корректно определять добавленные, удаленные и измененные строки. При миграции данных этот принцип сопоставления объектов по произвольным полям поиска реализован в обработке для переноса данных между похожими конфигурациями 1С.

    Преимущества: Универсальность, позволяет обрабатывать любые объекты без изменения кода для каждого типа.

    Недостатки: Сложность реализации, может быть менее производительным, чем точечный запрос, если проверяются все реквизиты.

Когда свойство Модифицированность все-таки полезно?

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

Заключение

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

← На главную