Версионирование регистров сведений

Моя реализация механизма мониторинга изменений регистра сведений.

Возникла ситуация, когда с некоторой периодичностью стали пропадать записи из регистра границы запрета изменения информации. Расследование не дало результатов, гугл решения подходящего не подсказал, поэтому пришлось делать свой механизм регистрации изменений.

Для начала был создан независимый периодический регистр сведений  История изменения объектов с измерением ИмяТаблицыМетаданных (строка 200) и ресурсами Изменения (Хранилище значения), Ответственный (Пользователь) и подписка события перед записью регистра сведений, изменения которого хотим отследить.

Листинг процедуры регистрации изменений:

Процедура ИсторияИзмененияРегистраСведений_ПередЗаписью(Источник, Отказ, Замещение) Экспорт
МД = Источник.Метаданные();
Текст = "";
ТекстЗапроса = "ВЫБРАТЬ" + ?(МД.ПериодичностьРегистраСведений=Метаданные.СвойстваОбъектов.ПериодичностьРегистраСведений.Непериодический,"",Символы.ПС + "Таб.Период,") + ?(ПолучитьТекстПоРеквизитам(МД,"Измерения",Текст),Текст,"") + ?(ПолучитьТекстПоРеквизитам(МД,"Ресурсы",Текст),","+Текст,"") + ?(ПолучитьТекстПоРеквизитам(МД,"Реквизиты",Текст),"," + Текст,"");
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Таб",Источник.Выгрузить());
СтрокаУсловий = "ИСТИНА";
СтрокаСоединений = "ИСТИНА";
Для Каждого ЭлементОтбора ИЗ Источник.Отбор Цикл
Если ЭлементОтбора.Использование Тогда
СтрокаУсловий = СтрокаУсловий + "
| И Таб." + ЭлементОтбора.Имя + " = &" + ЭлементОтбора.Имя;
Запрос.УстановитьПараметр(ЭлементОтбора.Имя,ЭлементОтбора.Значение);
КонецЕсли;
СтрокаСоединений = СтрокаСоединений + "
| И втТекущийНабор." + ЭлементОтбора.Имя + " = втНовыйНабор." + ЭлементОтбора.Имя;
КонецЦикла;
Запрос.Текст = ТекстЗапроса + "
| ПОМЕСТИТЬ втНовыйНабор
|ИЗ
| &Таб КАК Таб;
|"+ТекстЗапроса + "
| ПОМЕСТИТЬ втТекущийНабор
|ИЗ
| РегистрСведений." + МД.Имя + " КАК Таб
|ГДЕ " + СтрокаУсловий + ";
|ВЫБРАТЬ " + ?(МД.ПериодичностьРегистраСведений=Метаданные.СвойстваОбъектов.ПериодичностьРегистраСведений.Непериодический,"",Символы.ПС + "ISNULL(втТекущийНабор.Период,втНовыйНабор.Период) КАК Период,") + "
| " + ?(ПолучитьТекстПоРеквизитам(МД,"Измерения",Текст,"ISNULL(втТекущийНабор.%1,втНовыйНабор.%1) КАК %1"),Текст,"") + ?(ПолучитьТекстПоРеквизитам(МД,"Ресурсы",Текст,"втТекущийНабор.%1 КАК %1"),","+Текст,"") + ?(ПолучитьТекстПоРеквизитам(МД,"Реквизиты",Текст,"втТекущийНабор.%1 КАК %1"),"," + Текст,"")+?(ПолучитьТекстПоРеквизитам(МД,"Ресурсы",Текст,"втНовыйНабор.%1 КАК НовоеЗначение%1"),","+Текст,"") + ?(ПолучитьТекстПоРеквизитам(МД,"Реквизиты",Текст,"втНовыйНабор.%1 КАК НовоеЗначение%1"),"," + Текст,"")+",
| ВЫБОР КОГДА втТекущийНабор." + МД.Измерения[0].Имя + " IS NULL ТОГДА ""Добавление""
|  КОГДА втНовыйНабор." + МД.Измерения[0].Имя + " IS NULL ТОГДА ""Удаление""
|  ИНАЧЕ ""Изменение"" КОНЕЦ КАК Действие
|ИЗ
| втТекущийНабор
|  ПОЛНОЕ СОЕДИНЕНИЕ втНовыйНабор ПО " + СтрокаСоединений + "
|ГДЕ
| ИСТИНА " + ?(ПолучитьТекстПоРеквизитам(МД,"Ресурсы",Текст," И НЕ ISNULL(втТекущийНабор.%1,Неопределено) = ISNULL(втНовыйНабор.%1,Неопределено)", " "),Текст,"") + ?(ПолучитьТекстПоРеквизитам(МД,"Реквизиты",Текст," И НЕ ISNULL(втТекущийНабор.%1,Неопределено) = ISNULL(втНовыйНабор.%1,Неопределено)"," "),Текст,"");
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
ОбщиеПроцедурыПривилегированный.ЗарегистрироватьИзменениеРегистра(ПараметрыСеанса.ТекущийПользователь,МД.Имя,ТекущаяДата(),ПоместитьВоВременноеХранилище(Новый ХранилищеЗначения(РезультатЗапроса.Выгрузить())));
КонецЕсли;
КонецПроцедуры

Функция ПолучитьТекстПоРеквизитам(МД,Коллекция,Текст,ШаблонПараметра = "Таб.%1",Запятая = ",")
Текст = "";
Для Каждого Элемент ИЗ МД[Коллекция] Цикл;
Текст = Текст + "
| " + СтрЗаменить(ШаблонПараметра,"%1",Элемент.Имя) + Запятая;
КонецЦикла;
Текст = Лев(Текст,СтрДлина(Текст)-1);
Возврат НЕ ПустаяСтрока(Текст);
КонецФункции

и процедура в общем привилегированном модуле, чтобы не давать простым смертным доступа к регистру изменений

Процедура ЗарегистрироватьИзменениеРегистра(Пользователь,ИмяТаблицы,Дата,АдресИзменений)Экспорт
МЗ = РегистрыСведений.РК_ИсторияИзмененияРегистров.СоздатьМенеджерЗаписи();
МЗ.Активность = Истина;
МЗ.ИмяТаблицыМетаданных = ИмяТаблицы;
МЗ.Изменения = ПолучитьИзВременногоХранилища(АдресИзменений);
МЗ.Ответственный = Пользователь;
МЗ.Период = Дата;
МЗ.Записать(Истина);
КонецПроцедуры

Для вывода в формате отчета на форме списка регистра поместил табличное поле, заполняемое по требованию данными текущих изменений

Функция СформироватьОтчетПоСтроке(КлючЗаписи)
Изменения.Очистить();
Запись = РегистрыСведений.РК_ИсторияИзмененияРегистров.СоздатьМенеджерЗаписи();
ЗаполнитьЗначенияСвойств(Запись,КлючЗаписи);
Запись.Прочитать();
ТаблицаИзменений = Запись.Изменения.Получить();
СтруктураВнешнихДанных = Новый Структура("ТаблицаИзменений",ТаблицаИзменений);

СКД = Новый СхемаКомпоновкиДанных();
Источник = СКД.ИсточникиДанных.Добавить();
Источник.Имя = "ИсточникДанных";
Источник.СтрокаСоединения = "";
Источник.ТипИсточникаДанных = "Local";
НаборДанных = СКД.НаборыДанных.Добавить(Тип("НаборДанныхОбъектСхемыКомпоновкиДанных"));
НаборДанных.Имя = "ТаблицаИзменений";
НаборДанных.ИсточникДанных = "ИсточникДанных";
НаборДанных.ИмяОбъекта = "ТаблицаИзменений";
Для Каждого тКолонка ИЗ ТаблицаИзменений.Колонки Цикл
ДобавленноеПоле = НаборДанных.Поля.Добавить(Тип("ПолеНабораДанныхСхемыКомпоновкиДанных"));
ДобавленноеПоле.ПутьКДанным = тКолонка.Имя;
ДобавленноеПоле.Поле = тКолонка.Имя;
ДобавленноеПоле.ТипЗначения = тКолонка.ТипЗначения;
ВыбранноеПоле = СКД.ВариантыНастроек[0].Настройки.Выбор.Элементы.Добавить(Тип("ВыбранноеПолеКомпоновкиДанных"));
ВыбранноеПоле.Использование = Истина;
ВыбранноеПоле.Поле = Новый ПолеКомпоновкиДанных(тКолонка.Имя);
ВыбранноеПоле.Заголовок = тКолонка.Заголовок;
КонецЦикла;
ЭлементСтруктуры = СКД.ВариантыНастроек[0].Настройки.Структура.Добавить(Тип("ГруппировкаКомпоновкиДанных"));
ЭлементСтруктуры.Использование = Истина;
ЭлементСтруктуры.ПоляГруппировки.Элементы.Добавить(Тип("АвтоПолеГруппировкиКомпоновкиДанных")).Использование = Истина;
ЭлементСтруктуры.Выбор.Элементы.Добавить(Тип("АвтоВыбранноеПолеКомпоновкиДанных")).Использование = Истина;

Настройки = СКД.НастройкиПоУмолчанию;
ДанныеРасшифровкиСКД = Новый ДанныеРасшифровкиКомпоновкиДанных;
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СКД,Настройки, ДанныеРасшифровкиСКД);
ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновки,СтруктураВнешнихДанных ,ДанныеРасшифровкиСКД);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(Изменения);
ПроцессорВывода.Вывести(ПроцессорКомпоновкиДанных);
КонецФункции

ну и для красоты фильтр по регистрам, для которых есть отображение в истории

&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
Запрос = Новый Запрос;
Запрос.Текст = "Выбрать различные ИмяТаблицыМетаданных ИЗ РегистрСведений.ИсторияИзмененияРегистров";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Элементы.Регистр.СписокВыбора.Добавить(Выборка.ИмяТаблицыМетаданных,Метаданные.РегистрыСведений[Выборка.ИмяТаблицыМетаданных].Синоним);
КонецЦикла;
Регистр = Элементы.Регистр.СписокВыбора[0].Значение;
УстановитьОтборПоРегистру();
КонецПроцедуры

&НаСервере
Процедура УстановитьОтборПоРегистру()
Список.Параметры.УстановитьЗначениеПараметра("ИмяТаблицыМетаданных",Регистр);
КонецПроцедуры

Из недостатков можно отметить, что запись изменений делается до записи регистра и в случае, когда запись в отслеживаемый регистр по каким-либо причинам при записи не попадает, фиксация изменений имеет место, и второй основной недостаток это то, что в случае удаления ссылочного объекта из измерений/ресурсов/реквизитов отслеживаемого регистра в отчет будут выводиться битые ссылки.

9 Comments

  1. kosmo0

    Технологическая платформа

    Версия 8.3.11

    История данных.

    Реализован механизм отслеживания истории изменения объектов базы данных. Реализовано свойство История данных для следующих объектов конфигурации: справочники, документы, бизнес-процессы, задачи, регистры сведений.

    Реализована возможность использовать историю изменений на уровне платформы, за счет чего упрощается разработка и сопровождение прикладного решения.

    Случайно не подходит для решения подобных задач?

    Reply
  2. KonS

    (1)для случаев когда конфигурация позволяет включить режим совместимости с указанным релизом платформы и выше подходит.

    Reply
  3. Cmapnep

    1. Что будет при удалении записи? В подписке старых данных уже нет и нет никакой возможности выяснить что было в той записи, которая была удалена. Фактически регистрируется просто факт того, что кто-то что-то удалил — это имеет смысл только в каких-то очень простых случаях

    2. Что будет если в записи регистра сведений изменится значение в измерении, т.е. отборе? Фактически будет 2 записи — 1-ая об удалении чего-то, а 2-ая о появлении новой записи

    Также общее замечание — подход автора к динамическому формированию текста запроса весьма затрудняет понимание того, что хотел автор

    Reply
  4. KonS

    (3)1 процедура перед записью — ей передается новый набор, запросом из базы выбирается существующий набор, сравниваются и, в случае если по отбору набора записей новый набор пустой фиксируется удаление набора записей с указанием значений удаляемых записей. Для обозначенных в задаче целей этого более чем достаточно.

    2 как и работает механизм регистра в принципе — по одному набору измерений удаляется, по другому добавляется.

    Статическое указание запросов несколько ускоряет работу, значительно увеличивает объем кода и сужает сферу применения.

    Reply
  5. Cmapnep

    (4) Я указал конкретные проблемы, а в ответ получил описание принципа работы…

    Вероятно вы просто не поняли моих замечаний — попробую разжевать:

    1.

    процедура перед записью — ей передается новый набор, запросом из базы выбирается существующий набор

    — «существующий набор» получается по отборам нового набора, а если новый набор пустой, что и имеет место при удалении, то просто констатируется факт удаления, но не понятно что было удалено. Это и есть проблема №1. Надеюсь так понятней…

    2. Вторая проблема вот в чем — была запись, например, такая Изм1=А, Изм2=Б, Рес1=1, которую некий злоумышленник отредактировал, заменив в ней значение Изм2 так, что запись приобрела вид Изм1=А, Изм2=ББ, Рес1=1. При записи в подписке приведенный код зарегистрирует 2 факта: 1 — об удалении записи, причем что за запись удалилась будет неизвестно (см. описание п.1) и 2 — о появлении новой записи Изм1=А, Изм2=ББ, Рес1=1. Вы так и задумывали?

    Прочтите внимательно «подход автора к динамическому формированию текста запроса». Речь идет о том КАК формируется текст запроса. Каждому, как говорится, свое, но могу только предложить изучить подход к решению аналогичных задач, принятый в типовых конфигурациях.

    Reply
  6. KonS

    вы вероятно невнимательно читаете. давайте ещё раз

    1 перед записью выбирается набор из по переданным измерениям, сравнивается с записываемым набором и в лог сохраняются изменения, т е записи не совпадающие по значениям ресурсов и реквизитов. значения измерений берутся либо из считанного набора либо из нового, таким образом фиксируется что было добавлено/изменено/удалено

    2 Я вам так и написал — по одному набору измерений фиксируется удаление, по другому добавление. по аналогии как работает механизм менеджера записи.

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

    Reply
  7. Cmapnep

    (6) Действительно, я был невнимателен — алгоритм выполняется не при записи, а перед и там отборы заполнены старыми значениями.

    Достаточно ортодоксальный подход, но вероятно имеет право на жизнь. Приношу извинения

    Есть еще пара мелких проблем:

    — В начале обработчика подписки нужно добавить проверку на тип источника, чтобы исключить тип «РегистрСведенийНаборЗаписей.РК_ИсторияИзмененияРегистров»

    — В запросе нужно исключить реквизиты типа «ХранилищеЗначения», т.к. они вызывают ошибку в операциях сравнения

    И есть вопрос — почему история сохраняется только по одной записи в РС — той, что изменялась последней? В чем тут смысл?

    Reply
  8. KonS

    — Предполагается что конкретные выбранные регистры будут прописаны в подписке (что прописано в сопроводительном тексте) и проверка несколько бессмысленна. будет иметь смысл если заморачиваться с пользовательским интерфейсом настройки для выбора списка от

    Reply
  9. KonS

    (8)upd одно изменение РС в один момент времени конечно же. пользователь это ресурс

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *