В процессе сопровождения информационных систем на платформе «1С:Предприятие 8» разработчики часто сталкиваются с разбором конфликтных ситуаций: пользователи заявляют, что важные записи из регистров сведений пропали сами собой, и перекладывают ответственность друг на друга. Ситуация осложняется тем, что для независимых регистров сведений стандартный Журнал регистрации фиксирует операцию удаления крайне неинформативно — как событие «Данные.Изменение».
В этой статье мы подробно разберем, почему платформа ведет себя именно так, проанализируем технические нюансы работы транзакций СУБД при записи наборов данных и рассмотрим несколько эффективных способов решения проблемы: от использования встроенного механизма истории данных до написания собственных обработчиков и изменения архитектуры хранения информации.
Прежде чем переходить к написанию кода, давайте разберем физический смысл происходящего на уровне СУБД. В 1С запись в независимый регистр сведений (будь то через МенеджерЗаписи или через НаборЗаписей) всегда выполняется через транзакционное замещение. Платформа сначала очищает (удаляет) старые строки в таблице базы данных, которые соответствуют установленному отбору, а затем записывает новые строки.
Когда пользователь или алгоритм удаляет запись (например, записывает пустой набор записей с установленным отбором), для платформы это действие выглядит как штатная модификация данных: «записать 0 строк вместо 1 существовавшей ранее». Именно поэтому в Журнале регистрации событие помечается как модификация (изменение), а не удаление объекта. Понять из такого лога, кто именно очистил регистр и какие данные там находились, невозможно.
Ниже мы подробно рассмотрим современные и классические методы решения этой задачи.
---
Начиная с версии платформы 8.3.11, встроенный механизм «История данных» поддерживает работу с регистрами сведений. Это наиболее простое и производительное решение, не требующее написания сложного кода логирования.
Давайте разберем по шагам, как настроить и использовать этот механизм:
ГрафикСроковДоставки).ИсторияДанных.ОбновитьНастройки()).Как программно получить информацию об удаленной записи? Поскольку у записей регистра сведений нет уникальной ссылки (UUID), для обращения к ним используется специальный системный объект РегистрСведенийКлючЗаписи. Проанализируем пример кода, с помощью которого мы можем выгрузить историю изменений по конкретным измерениям регистра:
// Определяем ключевые измерения удаленной записи
Отбор = Новый Структура;
Отбор.Вставить("ТочкаДоставки", СсылкаНаТочку);
Отбор.Вставить("ДатаДоставки", ВыбраннаяДата);
// Создаем ключ записи для идентификации объекта в истории данных
КлючЗаписи = РегистрыСведений.ГрафикСроковДоставки.СоздатьКлючЗаписи(Отбор);
// Извлекаем версии из истории данных
ВыборкаВерсий = ИсторияДанных.ВыбратьВерсии(КлючЗаписи);
Пока ВыборкаВерсий.Следующий() Цикл
// Каждая итерация содержит метаданные о транзакции
Сообщить(СтрШаблон("Дата изменения: %1, Пользователь: %2, Версия: %3",
ВыборкаВерсий.Дата,
ВыборкаВерсий.Пользователь,
ВыборкаВерсий.Версия));
// Получаем состояние данных в этой версии
ДанныеВерсии = ВыборкаВерсий.Получить();
Если ДанныеВерсии = Неопределено Тогда
// Отсутствие данных при наличии версии указывает на факт удаления записи
Сообщить("На этом шаге запись была удалена!");
КонецЕсли;
КонецЦикла;
---
Если по каким-то причинам использование «Истории данных» невозможно (например, проект работает на старых версиях платформы или требуется писать лог в строго определенном формате во внешнюю систему), мы можем перехватить удаление программно. Для этого отлично подойдет подписка на событие ПередЗаписью набора записей регистра.
Важный нюанс: Не пытайтесь писать код логирования в формах (в событии таблицы ПередУдалением). Записи в регистре могут удаляться фоновыми заданиями, внешними обработками, API-сервисами или документами-регистраторами. Код в форме сработает только тогда, когда пользователь интерактивно нажмет кнопку «Удалить» на форме списка. Во всех остальных случаях удаление пройдет незамеченным.
Давайте спроектировать универсальный обработчик. При записи набора записей с установленным флагом Замещение = Истина старые данные еще физически находятся в базе данных. Мы можем успеть прочитать их до того, как платформа сотрет их из СУБД.
Создадим общий модуль ЛогированиеСобытийРегистров (с флагами «Сервер», «Вызов сервера») и подписку на событие ПередЗаписью для набора записей нашего регистра:
Процедура ПередЗаписьюНабораРегистра(Источник, Отказ, Замещение) Экспорт
// Исключаем обработку при стандартной загрузке данных (например, через РИБ)
Если Источник.ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Если замещение равно Истина, то старые строки по данному отбору будут удалены
Если Замещение Тогда
// Строим динамический запрос для получения текущих данных из базы
Запрос = Новый Запрос;
ТекстЗапроса =
"ВЫБРАТЬ
| ТаблицаРегистра.Период КАК Период,
| ТаблицаРегистра.ТочкаДоставки КАК ТочкаДоставки,
| ТаблицаРегистра.Перевозчик КАК Перевозчик,
| ТаблицаРегистра.СрокДоставки КАК СрокДоставки
|ИЗ
| РегистрСведений.ГрафикСроковДоставки КАК ТаблицаРегистра
|ГДЕ ";
// Формируем условия на основе активных отборов набора записей
СтрокаУсловий = "";
Для Каждого ЭлементОтбора Из Источник.Отбор Цикл
Если ЭлементОтбора.Использование Тогда
ИмяПараметра = ЭлементОтбора.Имя;
СтрокаУсловий = СтрокаУсловий + ?(ПустаяСтрока(СтрокаУсловий), "", " И ")
+ "ТаблицаРегистра." + ИмяПараметра + " = &" + ИмяПараметра;
Запрос.УстановитьПараметр(ИмяПараметра, ЭлементОтбора.Значение);
КонецЕсли;
КонецЦикла;
Если ПустаяСтрока(СтрокаУсловий) Тогда
// Если отбор пустой, значит происходит полная очистка регистра!
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "ГДЕ ", "");
Иначе
ТекстЗапроса = ТекстЗапроса + СтрокаУсловий;
КонецЕсли;
Запрос.Текст = ТекстЗапроса;
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Проверяем, это полная очистка (удаление) или просто модификация данных.
// Если в записываемом наборе записей (Источник) нет строк, значит происходит чистое удаление.
ЭтоУдаление = (Источник.Количество() = 0);
// Формируем детальное представление удаляемой строки в формате JSON
СтруктураДанных = Новый Структура;
СтруктураДанных.Вставить("Период", Выборка.Период);
СтруктураДанных.Вставить("ТочкаДоставки", СокрЛП(Выборка.ТочкаДоставки));
СтруктураДанных.Вставить("Перевозчик", СокрЛП(Выборка.Перевозчик));
СтруктураДанных.Вставить("СрокДоставки", Выборка.СрокДоставки);
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, СтруктураДанных);
СтрокаJSON = ЗаписьJSON.Закрыть();
// Фиксируем информацию в Журнале регистрации
ИмяСобытия = ?(ЭтоУдаление, "Регистр.УдалениеЗаписи", "Регистр.ИзменениеЗаписи");
ЗаписьЖурналаРегистрации(
ИмяСобытия,
УровеньЖурналаРегистрации.Предупреждение,
Метаданные.РегистрыСведений.ГрафикСроковДоставки,
,
"Удалены/изменены данные: " + СтрокаJSON
);
КонецЦикла;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Этот подход гарантирует, что даже если запись была стерта, ее точное содержимое в формате JSON со всеми ресурсами и реквизитами останется зафиксированным в Журнале регистрации вместе с именем пользователя и временем транзакции.
---
Давайте посмотрим на проблему шире. Если данные регистра «График сроков доставки» критически важны для бизнес-процессов компании, само физическое удаление строк из базы данных является небезопасной практикой. В крупных промышленных системах применяется концепция «Мягкого удаления» (Soft Delete).
Рассмотрим, как быстро перестроить логику работы регистра под этот паттерн:
Активно (тип: Булево) или Статус (тип: ПеречислениеСсылка.СтатусыЗаписей со значениями Действует и Удалено).Активно = Ложь.ГДЕ ТаблицаРегистра.Активно = ИСТИНА.Преимущества этого подхода очевидны: данные физически никогда не удаляются из СУБД. Любая смена активности записи автоматически фиксируется стандартным Журналом регистрации как штатное изменение («Данные.Изменение»), где четко видно автора операции. Также у вас всегда сохраняется полная история того, какими были графики доставок на любую прошедшую дату.
---
Иногда возникают экстренные ситуации, когда запись удаляется неизвестным фоновым процессом или кривым кодом внешней обработки, а вносить изменения в конфигурацию 1С «на лету» (снимать с поддержки, обновлять конфигурацию базы данных) нельзя из-за регламентов компании. Если ваша информационная база развернута в клиент-серверном варианте (на MS SQL Server или PostgreSQL), задачу можно решить на уровне СУБД.
Проанализируем этот сценарий на примере Microsoft SQL Server:
_InfoRgXXXX (где XXXX — внутренний числовой идентификатор объекта). Найти точное имя таблицы можно с помощью метода платформы ПолучитьСтруктуруХраненияБазыДанных().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С или внутри нее, не сможет удалить данные незаметно для администратора СУБД.
---
Какое решение выбрать для вашего проекта? Давайте подведем итоги: