gifts2017

Простой механизм версионирования для нетиповых конфигураций, или зачем нужны утки и медведи

Опубликовал Даниил Матвеев (cargobird) в раздел Программирование - Практика программирования

Предлагается вариант простого ненастраиваемого механизма версионирования, сделанного при помощи сравнения перед записью объекта и его ссылки по наименованиям реквизитов в метаданных, записи результата сравнения в журнал регистрации и последующего его извлечения.

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

Тем не менее, вопросы пользователей, кто изменил "мой" документ (справочник), а главное ЧТО изменил, остаются актуальными.

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

Суть в следующем.

В процедуре общего модуля по подписке на событие "ПередЗаписью" документа или справочника выполняем поиск различий по значениям совпадающих по наименованиям реквизитов метаданных объекта и ссылки (аналогичным образом выполняется и сравнение табличных частей).

Процедура ПередЗаписьюКонтрольИзмененияРеквизитовДокументов(Источник, Отказ) Экспорт

    Если НЕ Отказ И НЕ Источник.ЭтоНовый() Тогда
        ТЗРазличий = Новый ТаблицаЗначений;
        ТЗРазличий.Колонки.Добавить("ИмяРеквизита");
        ТЗРазличий.Колонки.Добавить("До");
        ТЗРазличий.Колонки.Добавить("После");
        МетаданныеДок = Источник.Метаданные();
        Для Каждого Реквизит Из МетаданныеДок.Реквизиты Цикл
            Если Источник[Реквизит.Имя] <> Источник.Ссылка[Реквизит.Имя] Тогда
                СтрокаТЗ = ТЗРазличий.Добавить();
                СтрокаТЗ.ИмяРеквизита = Реквизит.Имя;
                СтрокаТЗ.До = Строка(Источник[Реквизит.Имя]);
                СтрокаТЗ.После = Строка(Источник.Ссылка[Реквизит.Имя]);
            КонецЕсли;
        КонецЦикла;

...

КонецПроцедуры

Как видно из кода, значения реквизитов Источника, это то, что было ДО изменения, значения реквизитов Источник.Ссылка - это то, что БУДЕТ ПОСЛЕ записи изменения в случае, если объект таки будет записан.

По результатам анализа создаем структуру, содержащую в качестве значений таблицы различий реквизитов, а также таблицы изменений реквизитов табличных частей.

СтруктураРазличий = Новый Структура
СтруктураРазличий.Вставить("РазличияРеквизитов", ТЧРазличий)

Структуру помещаем в системное строковое представление при помощи ЗначениеВСтрокуВнутр.

СтруктураВнутр = ?(БылиИзменения, ЗначениеВСтрокуВнутр(СтруктураРазличий);

Делаем запись в журнале регистрации, где полученное представление помещаем в комментарий, указываем ссылку на измененный документ или справочник, и называем это вроде "Изменения реквизитов документа".

ЗаписьЖурналаРегистрации("Изменение реквизитов документа", УровеньЖурналаРегистрации.Информация, Источник.Метаданные(), Источник.Ссылка, СтруктураВнутр);

В дальнейшем при необходимости это представление извлекается,

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

вытаскивается из колонки "Комментарий", преобразовывается обратно в структуру через ЗначениеИзСтрокиВнутр

СтруктураИзВнутр = ЗначениеИзСтрокиВнутр(СтруктураВнутр);

и обратной обработкой развертывается в удобочитаемом виде.

Ну а дальше сложность анализа, как справедливо заметили коллеги к публикации с "уткой", может нарастать, как минимум должно быть нечеткое сравнение табличных частей, потому что уже выявлен недостаток вышеописанного метода: при добавлении строки в документ механизм фиксирует только изменение суммы документа, ведь первые N строк табличных частей до записи совпадают...

И последнее, к вопросу о скорости работы механизма. Видимого замедления записи или проведения объекта не замечено. Восстановление последовательности партионного учета при включенном механизме также прошло без замеченных тормозов. Механизм внедрен и работает на нетиповой УТ 10.3.

P.S. Закономерный вопрос: "Причем здесь утки?!"
Отвечаю. Если интересует полный код анализа различий по значениям совпадающих по наименованию реквизитов объектов, помещение в структуру и развертывание обратно из структуры с выводом в удобочитаемый вид, прошу сюда, в публикацию с "уткой":

http://infostart.ru/public/388917/

P.P.S. И второй закономерный вопрос: "Ну хорошо, а причем здесь медведи?!"
А это уже вариант, как может быть реализовано для конечного использования. В ту самую простую обработку, которая показывает изменения документа, можно добавить вывод вида события "Изменения реквизитов документа (справочника)" и по двойному щелчку развертывать расшифровку этих изменений. За обработкой прошу сюда, в публикацию с "медведями":

http://infostart.ru/public/372472/

См. также

Подписаться Добавить вознаграждение
Комментарии
1. Михаил Гусев (Идальго) 16.08.15 21:34
Мне кажется, что правильнее говорить не о версионировании, а о записи в ЖР информации об измененных значениях реквизитов.
2. Даниил Матвеев (cargobird) 16.08.15 21:45
(1) Идальго, да, согласен с вами, версионированием это можно назвать с очень большой натяжкой, нулевая точка отсчета для последующей возможной его реализации.
3. Даниил Матвеев (cargobird) 16.08.15 22:07
Еще один возможный минус такой записи изменений. Поскольку порядок выполнения однотипных подписок на событие не определен, вполне возможно, что после записи в журнал регистрации изменения объекта, следующая подписка на событие ПередЗаписью вызовет Отказ и объект не запишется. А в журнал регистрации уже попала запись об изменении объекта. То, что это "ложное срабатывание", можно будет определить только наличием после события об изменении реквизитов объекта события об отмене транзакции...
4. Александр Маляев (maljaev) 17.08.15 10:38
В моем варианте версионирования:

Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
	Если (Не Отказ) И (Не ОбменДанными.Загрузка) Тогда
		ВерсионныйКонтроль.ЗапомнитьМодифицированность(ЭтотОбъект);
	КонецЕсли;
КонецПроцедуры

Процедура ПриЗаписи(Отказ)
	Если (Не Отказ) И (Не ОбменДанными.Загрузка) Тогда
		ВерсионныйКонтроль.СоздатьВерсию(ЭтотОбъект);
	КонецЕсли;
КонецПроцедуры
...Показать Скрыть


Запоминается состояние документа в событии "ПередЗаписью", потом в событии "ПриЗаписи" (если не было отказов) сравниваются реквизиты и ТЧ целиком. Создается запись в специальном регистре сведений, туда записывается сам факт действия и все измененные реквизиты и ТЧ, упакованные в один пакет с максимальным сжатием. Потом можно сформировать как глобальный отчет по изменениям, так и по конкретному документу посмотреть/сравнить/откатить.
Прикрепленные файлы:
5. Александр Капустин (kapustinag) 17.08.15 10:52
(0) Сомнительное решение, по-моему. Журнал регистрации распухает очень быстрыми темпами в любой активно использующейся базе. А тут еще в него будут записываться довольно объемные записи. Причем, если такой механизм прикручен ко многим объектам, и их активно изменяют - таких записей будет много. В результате штатными средствами с Ж.Р. будет еще хуже работать.
Может быть, если Ж.Р переделан на хранение во внешней SQL-базе - есть публикация на Инфостарте об этом - будет нормально работать.
6. Даниил Матвеев (cargobird) 17.08.15 11:02
(4) maljaev, спасибо, интересно посмотреть другие варианты.
7. Даниил Матвеев (cargobird) 17.08.15 11:06
(5) kapustinag, регистрация изменений установлена только на критичные объекты - на несколько ключевых справочников и на все товаросодержащие документы, поэтому пока существенного увеличения ЖР не ожидаем. За наводку на SQL-ный журнал спасибо, мысль хорошая, подумаю.
8. Кирилл Бондаренко (karapuzzzz) 18.08.15 11:49
Как видно из кода, значения реквизитов Источника, это то, что было ДО изменения, значения реквизитов Источник.Ссылка - это то, что БУДЕТ ПОСЛЕ записи изменения в случае, если объект таки будет записан.

Скорее наоборот. Значение реквизитов Источника это то, что будет после принятия изменений. По ссылке мы обращаемся к записям БД, которые как раз и есть значениями до изменения
9. Кирилл Бондаренко (karapuzzzz) 18.08.15 11:53
Вместо журнала используйте регистр сведений, который можно периодически чистить. Отчет по записям регистра будет строиться несоизмеримо быстрее, чем анализировать записи журнала регистрации. Тем более, что подчищать регистр сведений опять таки проще.
10. Даниил Матвеев (cargobird) 18.08.15 15:52
(8) karapuzzzz, спасибо, посмотрю, может и правда перемудрил.
11. Даниил Матвеев (cargobird) 18.08.15 15:55
(9) karapuzzzz, ставилась цель без особых механизмов сделать простой инструмент для того, чтобы пользователь мог просматривать изменения документа. Поэтому решили по максимуму использовать штатные средства. Как я уже написал в (5), изменения регистрируются у нескольких справочников и документов, поэтому распухание ЖР скорее всего не грозит.
13. Дмитрий Воробьев (vde69) 19.08.15 08:38
во первых решение действительно "сомнительное", по тому как в реквизитах "ссылки" а не строки и там "моментов" много

во вторых банально туповато сделано в цикле....

я например вот так сделал (тут то же есть спорные моменты, но куда лучше чем дергать каждый реквизит отдельно):

	МассивНеПроверяемыхРеквизитов.Добавить("ИмяПредопределенныхДанных");
	МассивНеПроверяемыхРеквизитов.Добавить("Предопределенный");
	МассивНеПроверяемыхРеквизитов.Добавить("Ссылка");
	МассивНеПроверяемыхРеквизитов.Добавить("ЭтоГруппа");

	// проверяем, что в объекте действительно поменяли что-то важное
	Мета = Объект.Метаданные();
	Запрос = Новый Запрос;
	Запрос.Текст = 
	"ВЫБРАТЬ
	|	ИСТИНА КАК П
	|ИЗ
	|	"+Мета.ПолноеИмя()+" КАК Т
	|ГДЕ Т.Ссылка = &Ссылка 
	|	 И (ЛОЖЬ";	
	
	Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
	
	
	Для Каждого мРек из Мета.Реквизиты Цикл 
		мИмя = мРек.Имя;
		Если МассивНеПроверяемыхРеквизитов.Найти(мИмя) = Неопределено Тогда 
			Запрос.Текст = Запрос.Текст + " 
			|			ИЛИ Т."+мИмя+" <> &"+мИмя;
			Запрос.УстановитьПараметр(мИмя, Объект[мИмя]);
		КонецЕсли;
	КонецЦикла;
	
	Для Каждого мРек из Мета.СтандартныеРеквизиты Цикл 
		мИмя = мРек.Имя;
		Если МассивНеПроверяемыхРеквизитов.Найти(мИмя) = Неопределено Тогда 
			Запрос.Текст = Запрос.Текст + "
			|			ИЛИ Т."+мИмя+" <> &"+мИмя;
			Запрос.УстановитьПараметр(мИмя, Объект[мИмя]);
		КонецЕсли;
	КонецЦикла;
	
	Запрос.Текст = Запрос.Текст + ")";

	Если Запрос.Выполнить().Пустой() Тогда 
		Возврат; // изменений важных реквизитов не найдено....
	КонецЕсли;
...Показать Скрыть
14. Даниил Матвеев (cargobird) 19.08.15 08:48
(13) vde69, спасибо за уточнение, посмотрю. Пользователю, правда, на форме доступны именно те реквизиты, которые могут на что-то повлиять, остальные можно не трогать. А Полные права всегда что-то меняют под флагом ОбменДанными.Загрузка = Истина, поэтому их изменения не регистрируются.
15. Кирилл Бондаренко (karapuzzzz) 19.08.15 13:36
(11) cargobird, Если отбросить проблему распухания журнала регистрации, то остается еще 3:
1. Формирование отчета. Можно просмотреть информацию по документу из ЖР, но сформировать отчет можно быстрее. Даже не так. НАМНОГО БЫСТРЕЕ. Отчет предоставить больше данных. Это и отбор по периоду документа (ЖР такого не даст), и удобочитаемая форма, и представление ссылок как ссылок с возможностью открыть элемент справочника (например, если наименование у двух элементов одинаковое).
2. Разделение доступа. Человек, анализирующий записи может не иметь доступа на чтение ЖР и наоборот - человек, имеющий доступ к ЖР не должен иметь доступа к протоколу изменений (там же конкретные данные документов)
3. Очистка данных. Если использовать ЖР, то вместе с чисткой протокола изменений будет очищен и журнал. И наоборот.
16. Даниил Матвеев (cargobird) 19.08.15 14:18
(15) karapuzzzz, у нас была задача максимальной простоты реализации и максимального удобства для пользователя с минимумом выводимой информации.
Поэтому
1. Пользуемся вариантом обработки Кто сидел на моем стуле и сломал его? (анализ изменений документа по журналу регистрации). При выборе документа автоматически ставится период с даты документа по текущую дату, в общем случае этого достаточно, период при желании можно изменить.
2. В журнал регистрации никто не смотрит, т.к. большинство не умеют пользоваться отборами, работы хватает, не до этого и т.д.
3. Сейчас мы наоборот, на этапе накопления информации, вопрос пока не стоит.
В нашу специфику описанный метод вписывается вполне, осталось только сделать нормальный анализ таблиц.
17. Кирилл Бондаренко (karapuzzzz) 19.08.15 15:16
(16) cargobird, специфика журнала регистрации предполагает выборку всех данных, а потом ее анализ. Отбор можно установить только на конкретный документ, на пользователя, на период записей в журнале. Это и все, если не считать еще несколько отборов не нужных пользователям. Остальные отборы только на уже готовую выборку. А это время. Это очень большая нагрузка на систему.
Вы сейчас идете по принципу "система маленькая, нам хватает, развиваться не будем". Придет время рвать волосы и думать - чего раньше не сделали по уму. Я пытаюсь дать Вам понять, что реализация версионирования (протоколирования и т.п.) с использованием журнала регистрации это очень плохая идея. Многие настраивают перечень действий, вносимых в журнал регистрации, чтобы данных было минимум. Время от времени выгружают журнал и архивируют, оставляя только некий последний период. Все механизмы в системе стараются для ведения своего лога использовать свои таблицы, а не общую песочницу.
Если Вы уже добавили подписку на событие и процедуру в одном из общих модулей, что мешает добавить регистр сведений? Подумайте о своих коллегах, которым после Вас поддерживать продукт и анализировать, почему база пухнет. Простой анализ дает понять, что ссылка занимает меньше места, чем представление ссылки, а каждая запись журнала регистрации помимо осмысленных данных хранит еще кучу системной информации о клиенте, подключении и т.п. Строить правильную систему надо сначала, когда база это позволяет.
18. Даниил Матвеев (cargobird) 19.08.15 19:26
(17) karapuzzzz, я с вами ни в коей мере не спорю, коллега, а только обрисовываю ситуацию какая у нас сейчас.
Да, согласен, что в скором времени надо реализовывать более быстрый и оптимальный механизм.
Если резюмировать ваши комментарии, и комментарии коллеги в (4) и (13) надо реализовать примерно следующее:
1. В обработчике ПередЗаписью запомнить разницу между объектом и ссылкой, причем просматривать только необходимые реквизиты.
2. В обработчике ПриЗаписи, если не Отказ, окончательно записать изменения в свой созданный регистр сведений, желательно в реквизит типа ХранилищеЗначения чтобы иметь возможность упаковки с максимальной степенью сжатия:
Хранилище = Новый ХранилищеЗначения(Знач, Новый СжатиеДанных(9));

3. Затем обработкой/отчетом извлекать полученные данные, конвертируя их в удобочитаемый вид.
Так что есть над чем поработать, и спасибо всем за ценную информацию, это пригодится не только мне.
19. Вадим Никонов (V.Nikonov) 20.08.15 11:54
(18) cargobird, Есть ещё несколько соображений по поводу протоколирования:
1) Далеко не всегда известен объект анализа! Например, часто требуется найти все Документы в которых изменено количество некоторого Товара... Соответственно, изменяется Табличная Часть документа, реквизит Количество, а поиск удобнее организовать по ссылке Номенклатуры. При сворачивании Таблицы изменений - поиск по дополнительным признакам резко осложняется.
2) Штатное сравнение Табличных частей будет реагировать на изменение порядка строк в ТЧ! Частным случаем для накладных окажется изменение Единицы измерения с соответствующим пересчетом реквизита Количество...

Для борьбы с первой проблемой - указывал в ЖР ссылку Номенклатуры (вместо изменяемого документа), при сохранении ВидаМетаданных
Для борьбы со второй проблемой - особая ветка анализа ТЧ "Товары". Сравнивались Свернутые по измерениям (Номенклатура, Характеристика, Серия, Качество и т.п.) и приведенное к Единицам хранения количество... одновременно отбрасывались неактуальные реквизиты строк.
20. Даниил Матвеев (cargobird) 20.08.15 16:01
(19) V.Nikonov, да, предложенная в статье методика, что называется "просто и быстро", нулевой вариант. Отчета с отборами не получишь, а только фактическую информацию об изменениях всего чего можно. Изменения порядка строк в табличных частях выдадут большое количество изменений, среди которых нужное не найдешь и т.д.
Сейчас работаю над тем, чтобы этот вариант работал более-менее корректно с табличными частями. А там и до регистра сведений с настройками реквизитов вообще и табличных частей в частности доберемся.
За дельные советы спасибо)
21. Даниил Матвеев (cargobird) 23.10.15 13:44
Успешное использование механизма: менеджер случайно изменила в существующей позиции номенклатуры часть реквизитов - восстановили оригинал обратно в короткие сроки.
22. Даниил Матвеев (cargobird) 30.11.15 08:35
(4) maljaev, внедрил доп.контроль при записи, как у вас.
Обнаружил такую штуку.
Если отказ происходит в обработчике проведения (например, не заполнено количество), документ не записывается, но запись об изменении реквизитов все равно попадает в журнал регистрации (у вас - в механизм версионирования).
Делать дополнительную подписку на событие ПриПроведении тоже смысла нет, т.к. порядок подписок не определен.
Так что стопроцентного доп.контроля для документов, видимо, не сделать.
А для справочников годится.