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