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