При разработке систем логирования, аудита изменений или кастомных регистров истории в платформе 1С:Предприятие разработчики часто сталкиваются с неочевидным поведением независимых регистров сведений. Классическая проблема формулируется следующим образом: при изменении записи (особенно интерактивно через форму или при использовании менеджера записи) платформа выполняет запись в два этапа в рамках одной транзакции СУБД. Сначала вызывается обработчик события ПередЗаписью с пустым набором (для очистки старых данных по измерениям), а затем повторно вызывается тот же обработчик, но уже с заполненным набором, содержащим новые данные. В этой статье мы подробно разберем, почему так происходит, и рассмотрим несколько надежных архитектурных и программных решений этой задачи.
Давайте проанализируем внутреннюю кухню работы платформы с независимыми регистрами сведений. Когда пользователь или программный код изменяет существующую запись, СУБД должна физически обновить строки в таблице. В отличие от справочников или документов, у записи регистра сведений нет уникального идентификатора (GUID). Ключом записи является совокупность значений всех её измерений. Если в процессе изменения меняется хотя бы одно измерение, с точки зрения СУБД это уже совершенно другая запись.
Для обеспечения целостности данных платформа выполняет перезапись набора по следующему алгоритму:
ПередЗаписью. В этот момент коллекция ЭтотОбъект пуста (Количество() = 0). Это необходимо, чтобы удалить предыдущую версию записи из базы данных.ПередЗаписью, но теперь коллекция ЭтотОбъект содержит новые строки (Количество() > 0).Если же происходит обычное удаление записи (без последующего добавления новой), то выполняется только первый шаг — запись пустого набора. Для обработчика ПередЗаписью первый шаг удаления и первый шаг изменения выглядят абсолютно одинаково: мы получаем пустой набор записей. Из-за этого стандартное сравнение данных "напрямую" в модуле набора не позволяет сразу понять, удаляется ли запись окончательно или сейчас начнется второй шаг изменения. Для наглядного исследования последовательности срабатывания событий платформы можно использовать обработку для анализа порядка вызова событий 1С.
Прежде чем приступать к написанию собственного сложного кода, давайте рассмотрим штатную возможность платформы 1С, которая появилась начиная с версии 8.3.11. Механизм История данных (Data History) позволяет автоматически логировать любые изменения объектов конфигурации, включая независимые регистры сведений.
Платформа сама оптимизирует этот процесс, корректно обрабатывает транзакции и разделяет операции создания, изменения и удаления записей. Чтобы включить этот механизм, выполните следующие шаги:
Использовать.После этого платформа начнет автоматически сохранять версии записей. Этот способ идеален, если вам требуется просто предоставить пользователям или администраторам отчет об изменениях данных. Однако, если вам нужно выполнять кастомную бизнес-логику в момент удаления (например, отправлять REST-запрос во внешнюю систему или писать логи в строго определенном формате), штатного механизма может не хватить. Рассмотрим другие программные способы.
Если запись регистра сведений изменяется или удаляется программно (например, в модулях обмена, регламентных заданиях или обработках), самым простым и надежным способом является явное указание намерения разработчика с помощью коллекции ДополнительныеСвойства.
Перед вызовом метода Записать() у набора записей мы можем принудительно добавить свойство, которое будет доступно внутри обработчиков событий модуля этого набора. Рассмотрим пример кода при удалении записи:
НаборЗаписей = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Номенклатура.Установить(ВыбраннаяНоменклатура);
НаборЗаписей.Отбор.ТипЦен.Установить(ВыбранныйТипЦен);
// Явно указываем, что это операция удаления
НаборЗаписей.ДополнительныеСвойства.Вставить("ЭтоДействительноеУдаление", Истина);
НаборЗаписей.Записать();
Теперь в модуле набора записей регистра сведений в обработчике события ПередЗаписью мы можем легко перехватить этот флаг и однозначно определить характер операции:
Процедура ПередЗаписью(Отказ, Замещение)
Если ЭтотОбъект.ДополнительныеСвойства.Свойство("ЭтоДействительноеУдаление") Тогда
// Выполняем логику, предназначенную строго для удаления данных
ЗаписатьЛогУдаления(ЭтотОбъект.Отбор);
КонецЕсли;
КонецПроцедуры
Данный подход полностью исключает "угадывание" контекста на уровне базы данных, однако он требует строгой дисциплины разработки: вам придется контролировать все места в конфигурации, где программно изменяется или удаляется данный регистр сведений.
Что делать, если изменения происходят интерактивно через стандартные формы или внешние механизмы, где мы не можем напрямую прописать дополнительные свойства? В этом случае мы можем использовать транзакционный подход.
Поскольку обе итерации записи (очистка и последующее добавление) происходят строго в рамках одной транзакции базы данных, мы можем временно зафиксировать намерение удаления на первом шаге, а на втором шаге обновить статус лога. Разберем этот алгоритм по шагам:
ПередЗаписью (когда ЭтотОбъект.Количество() = 0) мы предполагаем, что запись действительно удаляется. Мы создаем запись в нашем регистре истории со статусом "Удалено". Чтобы связать эту запись со вторым возможным шагом, мы генерируем уникальный идентификатор транзакции или сеанса.ЭтотОбъект.Количество() > 0), мы понимаем, что это было не удаление, а обычное изменение. Мы находим созданную на первом шаге запись в истории по нашему идентификатору и меняем статус на "Изменено", заполняя новые значения полей.Посмотрим на пример реализации этой логики в модуле набора записей регистра сведений:
Процедура ПередЗаписью(Отказ, Замещение)
Если ЭтотОбъект.Количество() = 0 Тогда
// ШАГ 1: Подозрение на удаление
// Считываем старые данные из СУБД по текущему отбору набора
СтарыеДанные = ПолучитьТекущиеДанныеИзБазы(ЭтотОбъект.Отбор);
Если СтарыеДанные.Количество() > 0 Тогда
// Фиксируем в истории статус "Удалено"
ЗаписатьВИсторию(СтарыеДанные, "Удалено");
КонецЕсли;
Иначе
// ШАГ 2: Это было изменение (так как количество > 0)
// Находим ранее записанный лог "Удалено" в этой транзакции и перезаписываем его статус на "Изменено"
ПерезаписатьСтатусЛогаНаИзменение(ЭтотОбъект);
КонецЕсли;
КонецПроцедуры
Этот метод является одним из самых надежных, так как он опирается на транзакционную модель работы СУБД и не зависит от того, каким образом были изменены данные — интерактивно пользователем или через внешние источники.
Еще один продвинутый способ — сохранение состояния данных между вызовами обработчика в пределах одного сеанса. Для этого отлично подходит механизм Библиотеки Стандартных Подсистем (БСП) — буфер обмена, либо использование собственных параметров сеанса.
Суть метода заключается в том, что на первом шаге (при очистке) мы считываем текущие данные из базы данных запросом и сохраняем их во временное хранилище или параметр сеанса. На втором шаге мы извлекаем эти данные и сравниваем с новым набором записей. Если на втором шаге данные не появились в кэше или структура пуста, значит, произошло окончательное удаление.
Рассмотрим структуру реализации данного подхода:
ПередЗаписью при Количество() = 0 делаем запрос к таблице регистра с текущим отбором.ПараметрыСеанса.БуферРегистраСведений = Новый ХранилищеЗначения(ТаблицаСтарыхДанных);Количество() > 0) считываем данные из параметра сеанса, выполняем детальное сравнение полей (ресурсов и реквизитов) и определяем, какие именно поля изменились. После этого очищаем кэш.Мы детально проанализировали особенности работы независимых регистров сведений в 1С и разобрали, почему стандартная перезапись выглядит как удаление. Выбор конкретного метода зависит от архитектуры вашего решения: для простых систем идеально подойдет встроенный механизм «История данных», для строго контролируемых обменов — использование ДополнительныеСвойства, а для сложных систем кастомного аудита — транзакционный метод или кэширование через параметры сеанса.