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