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