Как правильно скопировать отбор набора записей в 1С и прочитать старые данные при записи?

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

В процессе разработки на платформе 1С:Предприятие часто возникает задача перехвата записи в регистры сведений или накопления. Типичный сценарий — в обработчике события ПередЗаписью или в соответствующей подписке на событие необходимо получить старое (текущее в СУБД) состояние записей до того, как они будут перезаписаны новыми данными. Для этого разработчику требуется создать новый вспомогательный набор записей, скопировать в него отбор из исходного набора и выполнить чтение.

На первый взгляд задача кажется тривиальной: достаточно обойти коллекцию отборов в цикле и скопировать их в новый набор. Однако на практике этот подход скрывает множество подводных камней, связанных с особенностями платформы 1С. В этой статье мы подробно разберем, как правильно копировать отборы, какие платформенные нюансы нужно учитывать и как гарантированно прочитать старые данные регистра перед записью.

Проблема наивного копирования отборов

Рассмотрим код, который часто используют начинающие разработчики (аналогичный код был предложен автором темы на форуме):


Процедура ПодпискаНаСобытиеПередЗаписью(Источник, Отказ, Замещение) Экспорт
    Набор = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
    Для каждого Эл Из Источник.Отбор Цикл
        Набор.Отбор[Эл.Имя].Установить(Эл.Значение);        
    КонецЦикла; 
    Набор.Прочитать();
КонецПроцедуры

Этот код содержит несколько критических уязвимостей, из-за которых логика приложения в определенный момент может дать сбой или вызвать аварийное завершение сеанса.

Разберем по шагам, почему этот алгоритм несовершенен и как его исправить.

Шаг 1. Учет активности элементов отбора (свойство Использование)

Первая и самая очевидная ошибка — игнорирование активности фильтра. Элемент отбора в исходном наборе записей может присутствовать в коллекции Источник.Отбор, но иметь признак Использование = Ложь. Это означает, что фактически фильтрация по данному полю отключена.

Когда мы вызываем метод Установить(Эл.Значение) с одним параметром, платформа 1С автоматически устанавливает свойство Использование нового отбора в значение Истина. В результате неактивный ранее фильтр становится активным, что полностью искажает исходный отбор и приводит к неверному чтению данных.

Правильное решение: всегда передавать признак использования в качестве второго параметра метода Установить().

Набор.Отбор[Эл.Имя].Установить(Эл.Значение, Эл.Использование);

Шаг 2. Разделение регистров по типу подчинения

Вторая проблема заключается в структуре самих регистров. Попытка обойти все элементы коллекции Источник.Отбор "в лоб" приведет к ошибке выполнения в зависимости от режима записи регистра:

  1. Регистры, подчиненные регистратору: Для таких регистров (как сведений, так и всех регистров накопления, расчета и бухгалтерии) отбор может быть установлен строго по полю "Регистратор". Попытка скопировать и принудительно установить отборы по измерениям вызовет системную ошибку: "Недопустимое поле отбора".
  2. Независимые регистры: Здесь отбор строится по измерениям. Также у независимых периодических регистров в отборе может присутствовать поле Период, которое тоже нужно корректно обрабатывать.

Поэтому перед копированием отбора мы должны проанализировать метаданные регистра и определить его тип.

Рассмотрим, как выглядит безопасный алгоритм копирования отбора с учетом структуры метаданных:


// Создаем новый набор записей того же типа
Набор = Регистры[Источник.Метаданные().Имя].СоздатьНаборЗаписей();

// Проверяем режим записи регистра
Если Источник.Метаданные().РежимЗаписи = РежимЗаписиРегистра.ПодчиненРегистратору Тогда
    // Для подчиненных регистров копируем только отбор по Регистратору
    Набор.Отбор.Регистратор.Установить(Источник.Отбор.Регистратор.Значение, Источник.Отбор.Регистратор.Использование);
Иначе
    // Для независимых регистров копируем отборы по измерениям и периоду
    Для каждого ЭлементОтбора Из Источник.Отбор Цикл
        ИмяПоля = ЭлементОтбора.Имя;
        Набор.Отбор[ИмяПоля].Установить(Источник.Отбор[ИмяПоля].Значение, Источник.Отбор[ИмяПоля].Использование);
    КонецЦикла;
КонецЕсли;

Шаг 3. Ловушка двухфазной записи (главный подводный камень)

Даже если мы идеально скопировали отбор, мы столкнемся с главной платформенной особенностью 1С:Предприятия — двухфазной записью наборов записей.

Если запись в регистр инициирована интерактивно пользователем через форму записи или программно через объект РегистрСведенийМенеджерЗаписи, платформа выполняет операцию в две фазы внутри одной транзакции:

В чем заключается ловушка: Если вы попытаетесь прочитать старые данные во время второго вызова события (когда Количество() > 0), метод Набор.Прочитать() вернет абсолютно пустой результат! Это происходит потому, что на первой фазе старые данные в СУБД уже были стерты. Читать старые данные необходимо строго на первой фазе — когда Источник.Количество() = 0.

Шаг 4. Передача прочитанных данных между фазами записи

Поскольку событие ПередЗаписью вызывается дважды, нам необходимо сохранить прочитанные на первой фазе старые данные, чтобы использовать их на второй фазе (когда записываются новые строки).

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

Для кэширования и передачи старых данных между фазами записи можно использовать следующие механизмы:

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

Шаг 5. Реализация надежного шаблона чтения старых данных

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

Ниже представлен пример реализации подписки на событие ПередЗаписью регистра сведений, которая корректно читает старые данные:


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

Использование этого шаблона позволит вам избежать неожиданных ошибок "Недопустимое поле отбора", гарантирует точное сохранение активности фильтров и предоставит разработчику надежный инструмент для сравнения старых и новых данных при записи любых регистров в системе 1С.

← На главную