Как определить, что документ изменен в модуле объекта в процедуре ПередЗаписью?

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

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

Почему стандартный метод Модифицированность() не подходит?

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

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

Стандарты разработки 1С:ИТС при проверке изменений

Фирма «1С» предъявляет жесткие требования к качеству кода при анализе изменений в транзакции записи. Проанализируем ключевое правило, которое часто нарушают разработчики.

Запрещено сохранять старые значения реквизитов в переменных модуля объекта для последующего сравнения. Некоторые программисты пытаются считать данные при чтении объекта (например, в событии ПриЧтенииНаСервере в модуле формы или через переменные модуля объекта) и запомнить их. Делать так категорически нельзя.

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

Единственно верный и безопасный способ — считывать исходные данные непосредственно из базы данных прямо в процедуре ПередЗаписью.

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

Решение 1. Сравнение простых реквизитов с помощью запроса

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

Если наш документ уже записан в базу (то есть ЭтоНовый() возвращает Ложь), мы можем сделать запрос к таблице этого документа, передав ссылку на текущий объект. Посмотрим на практический пример реализации такого алгоритма:


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

Решение 2. Оптимальное сравнение табличных частей через запрос

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

Суть метода заключается в следующем: мы берем оригинальные строки из базы данных и умножаем числовые показатели (например, Количество и Сумма) на -1. Текущие строки из оперативной памяти мы передаем в запрос как таблицу значений и оставляем числовые показатели со знаком 1. Затем мы объединяем обе таблицы через ОБЪЕДИНИТЬ ВСЕ и группируем по всем ключевым аналитическим полям (Номенклатура, Характеристика), суммируя показатели. Если табличные части идентичны, все показатели взаимно уничтожатся и дадут в сумме ноль. Если в результате запроса останется хотя бы одна строка с ненулевым итогом — табличная часть была изменена.

Рассмотрим пример реализации этого алгоритма:


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

Решение 3. Использование готовых инструментов БСП

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

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

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

Решение 4. Сравнение реквизитов с типом ХранилищеЗначений

Иногда документ содержит реквизиты с типом ХранилищеЗначений (например, файлы, изображения или сложные структуры настроек). Сравнить их простым равенством <> невозможно.

Для оптимизации работы с бинарными данными в современных версиях платформы (начиная с 8.3.10) следует извлекать данные напрямую методом ПолучитьДвоичныеДанныеИзХранилищаЗначений(). Это экономит память сервера, так как нам не нужно десериализовать объект в XML или Base64-строку. Для сравнения содержимого мы будем использовать криптографически стойкий хеш (например, SHA256).

Посмотрим на пример функции сравнения хранилищ значений:


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

Резюме и рекомендации

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

← На главную