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