Имя: Пароль:
1C
1С v8
Журналирование (логирование, протоколирование) изменений записей регистров сведе
0 a2a4
 
05.04.17
08:31
Наработки для хранения. В сети найти быстрее чем у себя :)



Процедура Журналирование_РегистрСведенийПередЗаписью(Источник, Отказ, Замещение) Экспорт
    
    ПорогДетальнойРегистрации = 5;//до этого количества (включительно) описываем каждую запись, свыше - без детализации, при ПорогДетальнойРегистрации=-1 журналируется всё
    ВыводНеЗаполненныхЗначенийРеквизитовРесурсов = Ложь;

    ПоляРегистраБезИзмерений = Источник.ВыгрузитьКолонки();
    ПоляИзмерений = Новый Структура;

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


Почему именно так.
1. Контроль необходим лишь для случаев расследования некорректного занесения данных (а это редкие, единичные случаи). Поэтому отметаем относительно сложные механизмы с выводом в внешний файл или специально предназначенные справочники или иные объекты базы данных. Просто пишем дополнительную информацию в журнал регистрации.
2.  Осуществление записи в журнал (журналирование) в принципе возможно:
- в объектах (документах, справочниках и проч. где изменяются регистры сведений);
- в модуле набра записей конкретного регистра сведений;
- при помощи механизма подписок в каком-либо общем модуле.
Универсально использовать механизм подписок. Во-первых, в этом случае вы "поймаете" абсолютно все изменения, как выполненные интерактивно так и программно (включая изменения пришедшие по обмену). Во-вторых, очень просто добавить/удалить журналирование какого-либо регистра сведений (просто добавляя/удаляя значения в источнике подписки на события).
3. Есть два ключевых события при изменении регистра сведений - ПередЗаписью и ПриЗаписи. Они имеют следующее различие. В событии ПередЗаписью есть возможность получить предыдущее значение (читая из базы данных). А для события ПриЗаписи "Процедура-обработчик вызывается после записи объекта в базу данных, но до окончания транзакции записи."
4. Установив РежимТранзакцииЗаписиЖурналаРегистрации.Транзакционная в процедуре ЗаписьЖурналаРегистрации получаем следующее. Во-первых, при отмене транзакции запись в журнале регистрации будет серого цвета, то есть выделяться визуально. Во-вторых, будет легче понимать записи журнала (особенно оба прохода при изменении регистров).
Для тех кто не в курсе - изменение записи регистра сведений осуществляется платформой 1с8 в два прохода - удаление предыдущей записи и добавление новой записи регистра сведений. ЗА ИСКЛЮЧЕНИЕМ РЯДА СЛУЧАЕВ ПРОГРАММНОГО ИЗМЕНЕНИЯ ДАННЫХ **.
5. Процедуру ЗаписьЖурналаРегистрации более корректно поместить в самом конце процедуры ПриЗаписи обработчика подписки на событие в виде Если Не Отказ Тогда. Так как в принципе в любом месте до этого может взвестить флаг Отказ=Истина и журналировать то, чего не произошло, бессмысленно. Для упрощения в данной публикации всё реализовано в одной процедуре. Но если вдруг собрались журналировать регистр сведений подчиненный регистратору, то перенсить процедуру ЗаписьЖурналаРегистрации в ПриЗаписи, так как в ряде случаев документ не будет проведен (а значит изменений регистра сведений не будет), а запись в журнале регистрации появится (пусть и с статусом транзакции Отменена).
Желающие могут  создать еще одну подписку на событие, с событием ПриЗаписи, и полностью совпадающим источником, и перенести в обработчик этой подписки на событие последний блок Если ... КонецЕсли. При этом СтрокиЖурнала передавать через ДополнительныеСвойства.
6. В ряде случаев возможно ограничивать журналирование. Например если регулярно удаляются тысячами строки регистров. И в подобном случае достаточно вывести в журнал одну запись, что удалено N записей регистра сведений по следующему отбору. В процедуре для этого введена переменная ПорогДетальнойРегистрации.
7. В записях журнала регистрации, в подробных сведениях дополнительно выводится код объектов. Если в этом нет необходимости, то достаточно закомментировать блоки Попытка ... КонецПопытки в двух местах.
8. Существует также два варианта вывода информации. Либо значения всех реквизитов и ресурсов либо только тех, которые имеют не пустые значения. Выбор осуществляется при помощи переменной ВыводНеЗаполненныхЗначенийРеквизитовРесурсов.
1 a2a4
 
05.04.17
08:33
Возможно некоторые заметили, что изменение данных регистра сведений записывается в журнал регистрации двумя записями. Почему это так. Изначально было желание выводить дополнительную информацию в следующем виде:
Сотрудник = "Иванов"->"Иванчук", Отработано = "8"->"6"
то есть наименование измерения/реквизита/ресурса и его значения - предыдущее и новое. Все наглядно и сразу понятно с первого взгляда. НО, не возможно. Вернее очень сложно реализовать. ИМХО, овчинка выделки не стоит.  (если кто-то сможет реализовать подобное - буду рад посмотреть).
1. Где реализовывать сравнение.
•    Во всех модулях объектов, в которых меняются данные регистра.
В этом случае нужен тщательный контроль за измением конфигурации (на случай возникновения нового места изменения регистра). Практически, забывается через 1-2 года. Плюс невозможность отследить изменения через внешние обработки.
•    В событиях ПередЗаписью и ПриЗаписи (как в модуле набора записей так и обработчиках подписки на события).
Универсально, но возникает проблема в п.2
2. Как получить одновременно и "старые" и "новые" данные. Изменение записей регистров сведений в большинстве случаев происходит за два прохода **. И если при первом проходе можно получить только "старые" данные (например запросом к БД), то при втором проходе только "новые" данные ***. Связать эти два прохода стандартными методами 1с8 невозможно (общее у них только номер транзакции). Напрашивается стандартное решение - при первом проходе сохранять предыдущие данные (в какой-либо переменной для временного хранения данных (глобальная переменную, константа, регистр сведений, хранилище значения (ПоместитьВоВременноеХранилище) и т.д.)). При этом возникают задачи.
•    Как определить что выполняется - просто удаление или первый проход при изменении данных. При "чистом удалении" надо сразу записывать в журнал регистрации, а при изменении данных - сохранять для "второго прохода".
Вроде был найден выход - метод Модифицированность(). В большинстве случаев при обычном удалении она в событии ПередЗаписью равна Истина, а при изменении в событии ПередЗаписью она равна Ложь. Но в ряде случаев программного "чистого" удаления ориентироваться на метод Модифицированность() некорректно***. Либо еще одно решение - при любом удалении сохранять в переменную. При "втором проходе" очищать переменную, а на случай "чистого удаления" запустить регламентированное задание или обработчик ожидания, который при наличии данных в переменной сделает запись в журнал регистрации. Но в этом случае не будет привязки к транзакции и надо будет отдельно отразить какой пользователь сделал изменение.
•    Как обеспечить однозначно соответствие между первым и вторым проходом. Можно по отбору (списку измерений) записи регистра сведений, но в регистре сведений могут изменить и значение измерения.
3. Как выполнять сравнеие. При изменении одной записи регистра проблем не существует. Если изменяется несколько записей, то может встать вопрос соответствия предыдущих и новых записей при изменении измерений регистра сведений. В ином случае делаем соответствие по отбору (списку измерений) записей регистров сведений.
4. Кроме того, если есть обмены данными (особенно РИБ) по которым приходят измененные значения регистра сведений, то получаем следующие исходные. Все изменения происходят в одной транзакции. Если было изменение измерения в записи регистра сведений, то по обмену придут два объекта - удаление предыдущей записи и добавление измененной записи (и как определить что это изменение одной записи).
В итоге, не получается получить код, который бы корректно отрабатывал 100% возможных вариантов. Поэтому "наглядный и понятный" вариант записи в журнал регистрации не получился.
Примечания:
* В процессе записи регистра сведений порядок событий следующий
ПередЗаписью (в модуле набора записей)->ПередЗаписью (обработчик подписки на событие)->ПриЗаписи (в модуле набора записей)->ПриЗаписи (обработчик подписки на событие).
** При изменении данных регистров сведений "... в общем случае, используются два набора записей: один предназначен для удаления "старой" записи, другой - для записи данных, определенных менеджером записи (или набором записей). Это проявляется, например, в том, что при выполнении записи могут дважды вызываться события ПередЗаписью и ПриЗаписи объекта РегистрСведенийНаборЗаписей, сначала для пустого набора записей удаляющего "старую" запись, а затем для набора записей с новыми данными". Иными словами изменение записи регистра сведений платформа 1с8 осуществляет за два прохода - (1) удаление предыдущих значений и (2) запись новых значений.  ЗА ИСКЛЮЧЕНИЕМ РЯДА СЛУЧАЕВ ПРОГРАММНОГО ИЗМЕНЕНИЯ ДАННЫХ.
2 a2a4
 
05.04.17
08:37
Интерактивное изменение значений регистров несколько отличается от некоторых случаев программного изменения.

В частности при следующем коде запись в БД будет осуществляться за один проход.

Набор=РегистрыСведений.ИмяРегистра.СоздатьНаборЗаписей();
Набор.Отбор.ИмяОтбора.Установить(ЗначениеОтбора);
Набор.Прочитать();
Набор.Удалить(ИндексЗаписи);
//и (или) изменить какую-либо запись
Набор.Записать();
3 a2a4
 
05.04.17
08:44
И по поводу признака Модифицированность(). В событиях ПередЗаписью и ПриЗаписи. В большинстве случаев Модифицированность=Ложь. Кроме

Создание нового.
ПередЗаписью    Модифицированность=Истина

Удаление.
ПередЗаписью    Модифицированность=Истина

Удаление и/или изменение при помощи кода
ПередЗаписью    Модифицированность=Истина

Набор=РегистрыСведений.ИмяРегистра.СоздатьНаборЗаписей();
Набор.Отбор.ИмяОтбора.Установить(ЗначениеОтбора);
Набор.Прочитать();
Набор.Удалить(ИндексЗаписи);
//и (или) изменить какую-либо запись
Набор.Записать();
4 Лефмихалыч
 
05.04.17
08:54
сюда сложи лучше
http://wiki.mista.ru/
читабельнее будет