Как сравнить новый и существующий наборы записей регистра сведений перед записью

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

При работе с объектами типа СправочникОбъект или ДокументОбъект разработчики привыкли использовать свойство Ссылка для получения состояния объекта до внесения изменений. Однако с регистрами сведений ситуация обстоит иначе. Объект РегистрСведенийНаборЗаписей не имеет встроенного механизма автоматического хранения «старых» данных. При вызове события ПередЗаписью в модуле набора мы видим только те данные, которые собираемся записать. Рассмотрим подробнее, как программно реализовать сравнение и выяснить, что именно изменилось в регистре.

Особенности работы набора записей в транзакции

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

Метод создания вспомогательного набора записей

Самый надежный способ получить текущее состояние базы данных без написания сложных запросов — это создание «зеркального» набора записей того же регистра. Разберем этот алгоритм по шагам:

  1. Внутри обработчика ПередЗаписью создаем новый экземпляр набора записей.
  2. Копируем отбор из текущего набора в новый.
  3. Вызываем метод Прочитать() для нового набора.
  4. Сравниваем содержимое двух коллекций.

Посмотрим на пример кода, реализующий этот подход для независимого регистра сведений:


Процедура ПередЗаписью(Отказ, Замещение)
    
    // 1. Создаем вспомогательный набор
    СтарыйНабор = РегистрыСведений.ИмяВашегоРегистра.СоздатьНаборЗаписей();
    
    // 2. Копируем отбор. 
    // Важно: нужно перенести значения всех активных элементов отбора.
    Для Каждого ЭлементОтбора Из ЭтотОбъект.Отбор Цикл
        Если ЭлементОтбора.Использование Тогда
            СтарыйНабор.Отбор[ЭлементОтбора.Имя].Установить(ЭлементОтбора.Значение);
        КонецЕсли;
    КонецЦикла;
    
    // 3. Читаем данные из базы
    СтарыйНабор.Прочитать();
    
    // 4. Теперь у нас есть СтарыйНабор (из базы) и ЭтотОбъект (новые данные)
    // Переходим к сравнению
    СравнитьНаборы(СтарыйНабор, ЭтотОбъект);
    
КонецПроцедуры

Алгоритм сравнения данных через таблицы значений

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

Проанализируем шаги эффективного сопоставления:

  1. Выгружаем оба набора записей в таблицы значений с помощью метода Выгрузить().
  2. Добавляем в каждую таблицу служебную колонку, например, СтатусЗаписи.
  3. В таблице со старыми данными заполняем эту колонку значением -1, в таблице с новыми — значением 1.
  4. Объединяем таблицы в одну общую ТЗ.
  5. Сворачиваем итоговую таблицу по всем измерениям, ресурсам и реквизитам, суммируя колонку СтатусЗаписи.

В результате свертки мы получим следующую картину:

Пример программной реализации сравнения

Рассмотрим реализацию функции, которая поможет автоматизировать этот процесс. Этот код можно разместить в общем модуле для универсального использования:


Функция ПолучитьИзмененияРегистра(СтарыйНабор, НовыйНабор)
    
    ТЗ_Старая = СтарыйНабор.Выгрузить();
    ТЗ_Новая = НовыйНабор.Выгрузить();
    
    ТЗ_Старая.Колонки.Добавить("СтатусИзменения", Новый ОписаниеТипов("Число"));
    ТЗ_Новая.Колонки.Добавить("СтатусИзменения", Новый ОписаниеТипов("Число"));
    
    ТЗ_Старая.ЗаполнитьЗначения(-1, "СтатусИзменения");
    ТЗ_Новая.ЗаполнитьЗначения(1, "СтатусИзменения");
    
    // Объединяем
    Для Каждого СтрокаНовая Из ТЗ_Новая Цикл
        ЗаполнитьЗначенияСвойств(ТЗ_Старая.Добавить(), СтрокаНовая);
    КонецЦикла;
    
    // Формируем список колонок для свертки (все, кроме служебной)
    КолонкиДляСвертки = "";
    Для Каждого Колонка Из ТЗ_Старая.Колонки Цикл
        Если Колонка.Имя <> "СтатусИзменения" Тогда
            КолонкиДляСвертки = КолонкиДляСвертки + Колонка.Имя + ",";
        КонецЕсли;
    КонецЦикла;
    КолонкиДляСвертки = Лев(КолонкиДляСвертки, СтрДлина(КолонкиДляСвертки)-1);
    
    ТЗ_Старая.Свернуть(КолонкиДляСвертки, "СтатусИзменения");
    
    // Удаляем неизмененные строки (где сумма 0)
    Индекс = ТЗ_Старая.Количество() - 1;
    Пока Индекс >= 0 Цикл
        Если ТЗ_Старая[Индекс].СтатусИзменения = 0 Тогда
            ТЗ_Старая.Удалить(Индекс);
        КонецЕсли;
        Индекс = Индекс - 1;
    КонецЦикла;
    
    Возврат ТЗ_Старая;
    
КонецФункции

Нюансы производительности и ограничения

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

В таких случаях запрос действительно является более предпочтительной альтернативой, так как он позволяет:

Однако для небольших регистров или при записи с узким отбором (например, по конкретному Объекту или Периоду), использование вспомогательного объекта НаборЗаписей является более "чистым" с точки зрения архитектуры 1С кодом, так как он универсально обрабатывает структуру измерений без необходимости динамического формирования текста запроса.

Работа с ДополнительнымиСвойствами

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


// В месте инициации записи
Набор = РегистрыСведений.Цены.СоздатьНаборЗаписей();
Набор.Отбор.Номенклатура.Установить(ТекущаяНоменклатура);
// Читаем старые данные заранее, если они нужны для логики
СтарыеДанные = Набор.Выгрузить(); 
Набор.ДополнительныеСвойства.Вставить("ТаблицаСтарыхЗначений", СтарыеДанные);
// Добавляем новые данные и записываем
...
Набор.Записать();

// В модуле набора записей
Процедура ПередЗаписью(Отказ, Замещение)
    Если ДополнительныеСвойства.Свойство("ТаблицаСтарыхЗначений") Тогда
        ТЗ_Старая = ДополнительныеСвойства.ТаблицаСтарыхЗначений;
        // Используем готовую таблицу для сравнения
    КонецЕсли;
КонецПроцедуры

Подведем итог: для сравнения набора записей регистра сведений необходимо вручную получить состояние базы данных. Использование вспомогательного набора с методом Прочитать() — самый простой путь, а алгоритм со статусами 1 / -1 в таблице значений — самый быстрый способ сопоставления структуры данных внутри кода.

← На главную