Как зафиксировать и логировать удаление записей в независимом регистре сведений в 1С

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

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

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

Почему стандартный журнал регистрации пишет «Данные.Изменение» при удалении?

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

Когда пользователь или алгоритм удаляет запись (например, записывает пустой набор записей с установленным отбором), для платформы это действие выглядит как штатная модификация данных: «записать 0 строк вместо 1 существовавшей ранее». Именно поэтому в Журнале регистрации событие помечается как модификация (изменение), а не удаление объекта. Понять из такого лога, кто именно очистил регистр и какие данные там находились, невозможно.

Ниже мы подробно рассмотрим современные и классические методы решения этой задачи.

---

Решение 1. Использование встроенного механизма «История данных» (Платформа 8.3.11 и выше)

Начиная с версии платформы 8.3.11, встроенный механизм «История данных» поддерживает работу с регистрами сведений. Это наиболее простое и производительное решение, не требующее написания сложного кода логирования.

Давайте разберем по шагам, как настроить и использовать этот механизм:

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

Как программно получить информацию об удаленной записи? Поскольку у записей регистра сведений нет уникальной ссылки (UUID), для обращения к ним используется специальный системный объект РегистрСведенийКлючЗаписи. Проанализируем пример кода, с помощью которого мы можем выгрузить историю изменений по конкретным измерениям регистра:


// Определяем ключевые измерения удаленной записи
Отбор = Новый Структура;
Отбор.Вставить("ТочкаДоставки", СсылкаНаТочку);
Отбор.Вставить("ДатаДоставки", ВыбраннаяДата);

// Создаем ключ записи для идентификации объекта в истории данных
КлючЗаписи = РегистрыСведений.ГрафикСроковДоставки.СоздатьКлючЗаписи(Отбор);

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

---

Решение 2. Создание подписки на событие «ПередЗаписью» (Ручное логирование)

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

Важный нюанс: Не пытайтесь писать код логирования в формах (в событии таблицы ПередУдалением). Записи в регистре могут удаляться фоновыми заданиями, внешними обработками, API-сервисами или документами-регистраторами. Код в форме сработает только тогда, когда пользователь интерактивно нажмет кнопку «Удалить» на форме списка. Во всех остальных случаях удаление пройдет незамеченным.

Давайте спроектировать универсальный обработчик. При записи набора записей с установленным флагом Замещение = Истина старые данные еще физически находятся в базе данных. Мы можем успеть прочитать их до того, как платформа сотрет их из СУБД.

Создадим общий модуль ЛогированиеСобытийРегистров (с флагами «Сервер», «Вызов сервера») и подписку на событие ПередЗаписью для набора записей нашего регистра:


Процедура ПередЗаписьюНабораРегистра(Источник, Отказ, Замещение) Экспорт
    
    // Исключаем обработку при стандартной загрузке данных (например, через РИБ)
    Если Источник.ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Если замещение равно Истина, то старые строки по данному отбору будут удалены
    Если Замещение Тогда
        
        // Строим динамический запрос для получения текущих данных из базы
        Запрос = Новый Запрос;
        ТекстЗапроса = 
            "ВЫБРАТЬ
            |	ТаблицаРегистра.Период КАК Период,
            |	ТаблицаРегистра.ТочкаДоставки КАК ТочкаДоставки,
            |	ТаблицаРегистра.Перевозчик КАК Перевозчик,
            |	ТаблицаРегистра.СрокДоставки КАК СрокДоставки
            |ИЗ
            |	РегистрСведений.ГрафикСроковДоставки КАК ТаблицаРегистра
            |ГДЕ ";
            
        // Формируем условия на основе активных отборов набора записей
        СтрокаУсловий = "";
        Для Каждого ЭлементОтбора Из Источник.Отбор Цикл
            Если ЭлементОтбора.Использование Тогда
                ИмяПараметра = ЭлементОтбора.Имя;
                СтрокаУсловий = СтрокаУсловий + ?(ПустаяСтрока(СтрокаУсловий), "", " И ") 
                                + "ТаблицаРегистра." + ИмяПараметра + " = &" + ИмяПараметра;
                Запрос.УстановитьПараметр(ИмяПараметра, ЭлементОтбора.Значение);
            КонецЕсли;
        КонецЦикла;
        
        Если ПустаяСтрока(СтрокаУсловий) Тогда
            // Если отбор пустой, значит происходит полная очистка регистра!
            ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "ГДЕ ", "");
        Иначе
            ТекстЗапроса = ТекстЗапроса + СтрокаУсловий;
        КонецЕсли;
        
        Запрос.Текст = ТекстЗапроса;
        РезультатЗапроса = Запрос.Выполнить();
        
        Если Не РезультатЗапроса.Пустой() Тогда
            Выборка = РезультатЗапроса.Выбрать();
            Пока Выборка.Следующий() Цикл
                
                // Проверяем, это полная очистка (удаление) или просто модификация данных.
                // Если в записываемом наборе записей (Источник) нет строк, значит происходит чистое удаление.
                ЭтоУдаление = (Источник.Количество() = 0);
                
                // Формируем детальное представление удаляемой строки в формате JSON
                СтруктураДанных = Новый Структура;
                СтруктураДанных.Вставить("Период", Выборка.Период);
                СтруктураДанных.Вставить("ТочкаДоставки", СокрЛП(Выборка.ТочкаДоставки));
                СтруктураДанных.Вставить("Перевозчик", СокрЛП(Выборка.Перевозчик));
                СтруктураДанных.Вставить("СрокДоставки", Выборка.СрокДоставки);
                
                ЗаписьJSON = Новый ЗаписьJSON;
                ЗаписьJSON.УстановитьСтроку();
                ЗаписатьJSON(ЗаписьJSON, СтруктураДанных);
                СтрокаJSON = ЗаписьJSON.Закрыть();
                
                // Фиксируем информацию в Журнале регистрации
                ИмяСобытия = ?(ЭтоУдаление, "Регистр.УдалениеЗаписи", "Регистр.ИзменениеЗаписи");
                
                ЗаписьЖурналаРегистрации(
                    ИмяСобытия,
                    УровеньЖурналаРегистрации.Предупреждение,
                    Метаданные.РегистрыСведений.ГрафикСроковДоставки,
                    ,
                    "Удалены/изменены данные: " + СтрокаJSON
                );
            КонецЦикла;
        КонецЕсли;
        
    КонецЕсли;
    
КонецПроцедуры

Этот подход гарантирует, что даже если запись была стерта, ее точное содержимое в формате JSON со всеми ресурсами и реквизитами останется зафиксированным в Журнале регистрации вместе с именем пользователя и временем транзакции.

---

Решение 3. Архитектурный подход «Мягкое удаление»

Давайте посмотрим на проблему шире. Если данные регистра «График сроков доставки» критически важны для бизнес-процессов компании, само физическое удаление строк из базы данных является небезопасной практикой. В крупных промышленных системах применяется концепция «Мягкого удаления» (Soft Delete).

Рассмотрим, как быстро перестроить логику работы регистра под этот паттерн:

  1. Добавим в регистр сведений новый реквизит или измерение Активно (тип: Булево) или Статус (тип: ПеречислениеСсылка.СтатусыЗаписей со значениями Действует и Удалено).
  2. Вместо вызова метода удаления записей, все прикладные алгоритмы и интерфейсные действия пользователей переписываем на изменение флага: Активно = Ложь.
  3. Все запросы, отчеты, динамические списки и механизмы проведения документов адаптируем так, чтобы они считывали данные только с фильтром ГДЕ ТаблицаРегистра.Активно = ИСТИНА.

Преимущества этого подхода очевидны: данные физически никогда не удаляются из СУБД. Любая смена активности записи автоматически фиксируется стандартным Журналом регистрации как штатное изменение («Данные.Изменение»), где четко видно автора операции. Также у вас всегда сохраняется полная история того, какими были графики доставок на любую прошедшую дату.

---

Решение 4. Логирование на уровне СУБД (SQL-триггеры)

Иногда возникают экстренные ситуации, когда запись удаляется неизвестным фоновым процессом или кривым кодом внешней обработки, а вносить изменения в конфигурацию 1С «на лету» (снимать с поддержки, обновлять конфигурацию базы данных) нельзя из-за регламентов компании. Если ваша информационная база развернута в клиент-серверном варианте (на MS SQL Server или PostgreSQL), задачу можно решить на уровне СУБД.

Проанализируем этот сценарий на примере Microsoft SQL Server:

  1. Через консоль SQL Server Management Studio найдем физическую таблицу нашего регистра. В базах 1С таблицы независимых регистров сведений именуются по шаблону _InfoRgXXXX (где XXXX — внутренний числовой идентификатор объекта). Найти точное имя таблицы можно с помощью метода платформы ПолучитьСтруктуруХраненияБазыДанных().
  2. Создадим специальный триггер на операцию AFTER DELETE, который при любой попытке удаления строки будет автоматически копировать удаленные данные во вспомогательную (теневую) таблицу логирования.

Посмотрим на пример SQL-скрипта для создания такого триггера:


CREATE TRIGGER tr_InfoRg1234_DeleteLog
ON _InfoRg1234
AFTER DELETE
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Записываем удаленные строки в специальную лог-таблицу
    INSERT INTO CustomDeleteLogTable (DeletedDate, RecordKey, DeletedDataDump, DbUser)
    SELECT 
        GETDATE(), 
        d._Fld567, -- Пример поля измерения (Код точки)
        (SELECT d.* FOR XML PATH('Record')), -- Сериализуем всю строку в XML
        ORIGINAL_LOGIN() -- Системное имя учетной записи СУБД или процесса
    FROM deleted d;
END;

Этот метод гарантирует 100% фиксацию факта удаления на уровне ядра СУБД. Ни один процесс, запущенный в обход платформы 1С или внутри нее, не сможет удалить данные незаметно для администратора СУБД.

---

Резюме и рекомендации

Какое решение выбрать для вашего проекта? Давайте подведем итоги:

← На главную