При разработке подсистем аудита и логирования изменений в регистрах сведений разработчики часто сталкиваются с задачей сопоставления значений: необходимо зафиксировать в журнале регистрации или отдельной таблице состояние полей в формате было / стало. Однако при интерактивном редактировании записей через форму или программной работе с менеджером записи платформа 1С выполняет операцию в два этапа: сначала удаляется старая запись, затем добавляется новая. Разберем, почему это происходит, как обойти возникающие ограничения платформы и какими способами можно организовать временное хранение промежуточных данных без изменения структуры метаданных.
Проанализируем механику работы объекта РегистрСведенийМенеджерЗаписи, который платформа использует при редактировании через форму записи или при вызове соответствующих методов. Чтобы гарантировать целостность данных при изменении ключевых полей (измерений или периода), платформа последовательно формирует два совершенно разных набора записей РегистрСведенийНаборЗаписей в рамках единой транзакции:
Поскольку это два независимых программных объекта, коллекция ДополнительныеСвойства первого набора записей уничтожается вместе с ним при завершении его записи. Передать в ней старые данные во второй шаг невозможно. Кроме того, метод ПоместитьВоВременноеХранилище() не принимает произвольные строки в качестве первого параметра (адреса) — адресом может выступать только уникальный системный идентификатор или идентификатор формы, что исключает прямую привязку по строковому ключу отбора.
Разберем вариант временного сохранения состояния между вызовами процедур с помощью серверного общего модуля с признаком Повторное использование возвращаемых значений (на время вызова или на время сеанса). Такой подход позволяет аккумулировать коллекции в памяти сервера без внесения изменений в структуру хранимых таблиц.
Создадим или используем общий модуль с включенным повторным использованием и опишем в нем функцию, возвращающую коллекцию:
Функция БуферИзмененийРегистров() Экспорт
// Возвращаем соответствие или структуру, существующую в рамках кеша сеанса
Возврат Новый Соответствие;
КонецФункции
Посмотрим на практическое применение этого буфера в подписке на события или модуле набора записей. В обработчике ПередЗаписью первого этапа (когда набор пустой, то есть происходит удаление старой записи) мы считываем текущие данные из базы и помещаем их в буфер:
Процедура РегистрСведенийПередЗаписью(Источник, Отказ, Замещение) Экспорт
// Формируем уникальный ключ записи по измерениям отбора
КлючЗаписи = ПолучитьКлючПоОтбору(Источник.Отбор);
Буфер = ОбщегоНазначенияПовтИсп.БуферИзмененийРегистров();
Если Источник.Количество() = 0 Тогда
// Первый этап: очистка старых данных. Считываем текущее состояние из базы
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| *
|ИЗ
| РегистрСведений.НормыРасхода КАК Регистр
|ГДЕ
| Регистр.Период = &Период
| И Регистр.МодельТС = &МодельТС";
Запрос.УстановитьПараметр("Период", Источник.Отбор.Период.Значение);
Запрос.УстановитьПараметр("МодельТС", Источник.Отбор.МодельТС.Значение);
Выборка = Запрос.Выполнить().Выгрузить();
Если Выборка.Количество() > 0 Тогда
Буфер.Вставить(КлючЗаписи, Выборка[0]);
КонецЕсли;
Иначе
// Второй этап: запись новых значений
СтарыеДанные = Буфер[КлючЗаписи];
Если СтарыеДанные <> Неопределено Тогда
НоваяЗапись = Источник[0];
// Сравниваем ресурсы и реквизиты
Если СтарыеДанные.ЛинейнаяНорма <> НоваяЗапись.ЛинейнаяНорма Тогда
ТекстЛога = СтрШаблон(
"ЛинейнаяНорма: было %1 / стало %2",
СтарыеДанные.ЛинейнаяНорма,
НоваяЗапись.ЛинейнаяНорма
);
ЗаписьЖурналаРегистрации(
"Изменение регистра сведений",
УровеньЖурналаРегистрации.Информация,,,
ТекстЛога
);
КонецЕсли;
// Очищаем буфер после сопоставления
Буфер.Удалить(КлючЗаписи);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Рассмотрим альтернативный способ передачи данных в рамках одного серверного вызова. Если весь процесс выполняется в рамках одной транзакции и одного контекста выполнения, мы можем объявить глобальную или экспортную переменную модуля:
Перем БуферЛогирования Экспорт;
Процедура ИнициализироватьБуфер() Экспорт
Если БуферЛогирования = Неопределено Тогда
БуферЛогирования = Новый Соответствие;
КонецЕсли;
КонецПроцедуры
Этот способ отличается минимальными накладными расходами памяти, однако требует аккуратности: необходимо гарантировать очистку данных в обработчиках ошибок или по завершении записи, чтобы исключить утечки памяти и случайные коллизии между сеансами.
Выясним, как ведет себя платформа, если данные записываются не через интерактивную форму, а чисто программно с использованием объекта РегистрСведенийНаборЗаписей. При использовании набора записей с параметром Замещение = Истина двойного вызова обработчиков не происходит:
Набор = РегистрыСведений.НормыРасхода.СоздатьНаборЗаписей();
Набор.Отбор.МодельТС.Установить(ВыбраннаяМодель);
Набор.Прочитать();
// Вносим изменения в коллекцию строк
Набор[0].ЛинейнаяНорма = НоваяНорма;
// Записываем набор за один проход
Набор.Записать(Истина);
В этом сценарии в обработчике события ПередЗаписью физические строки таблицы СУБД еще не заменены. Мы можем получить исходные строки простым запросом к регистру по текущему отбору набора и сразу же сопоставить их с коллекцией строк, содержащейся непосредственно в свойстве Источник (или ЭтотОбъект), без использования внешних промежуточных буферов.
Если конфигурация работает на современной версии платформы 1С:Предприятие 8.3, для независимых регистров сведений рекомендуется использовать штатный системный механизм История данных. При его включении платформа берет на себя все низкоуровневые операции версионирования:
Использование встроенной истории данных исключает необходимость ручного кеширования промежуточных состояний и полностью решает проблему журналирования изменений регистров сведений.