При разработке конфигураций с нуля или доработке существующих систем часто возникает задача отслеживать историю изменений документов: кто, когда и какие именно реквизиты или строки табличных частей изменил. В платформе 1С 8.3 и типовых решениях существует несколько проверенных способов реализации этого функционала.
В этой статье мы подробно разберем три основных подхода к созданию журнала изменений, проанализируем их плюсы и минусы, а также приведем готовые примеры кода для реализации собственной системы логирования.
---
Начиная с версии платформы 1С 8.3.11, разработчикам доступен мощный встроенный механизм — История данных. Это наиболее современный и производительный способ версионирования, работающий на уровне СУБД из коробки.
Рассмотрим ключевые преимущества этого механизма:
Проанализируем процесс включения механизма «История данных» по шагам:
Для разработчиков платформа предоставляет гибкий программный интерфейс. Например, мы можем использовать событие ОбработкаФормированияПоВерсии в модуле объекта, чтобы программно вмешаться в процесс записи версии и добавить сопутствующие данные (например, IP-адрес пользователя или информацию из внешних систем интеграции):
Процедура ОбработкаФормированияПоВерсии(Версия, ИсточникДанных, Отказ)
// Пример добавления дополнительных данных в историю
ДополнительныеДанные = Новый Структура;
ДополнительныеДанные.Вставить("ИмяКомпьютера", ИмяКомпьютера());
ДополнительныеДанные.Вставить("IPАдрес", "192.168.1.105"); // Пример статического или полученного программно IP
ЗаписьИсторииДанных = Новый ЗаписьИсторииДанных;
ЗаписьИсторииДанных.ДобавитьДополнительныеДанные(ДополнительныеДанные);
КонецПроцедуры
---
Если ваша конфигурация разработана на базе БСП (Библиотеки стандартных подсистем), вам не нужно изобретать велосипед. БСП содержит готовую подсистему «Версионирование объектов».
Принцип работы БСП заключается в следующем:
В момент записи объекта срабатывает подписка на событие ПриЗаписи. Система сериализует весь документ в формат XML (или Fastinfoset) и записывает его в регистр сведений ВерсииОбъектов. Для пользователя доступен удобный интерфейс сравнения версий, где подсвечиваются измененные реквизиты.
Однако у этого метода есть существенный недостаток: запись происходит синхронно внутри транзакции сохранения документа. Если у вас интенсивный документооборот или выполняются массовые групповые обработки, это может вызвать сильное замедление работы системы.
---
Если вы разрабатываете небольшую конфигурацию с нуля и не хотите перегружать ее тяжелыми подсистемами БСП, лучшим выбором будет создание собственного простого регистра изменений.
Выясним структуру нашего будущего регистра сведений. Создадим непериодический независимый регистр сведений ЖурналИзмененийДокументов со следующей структурой:
Измерения:
СсылкаНаОбъект (Тип: ЛюбаяСсылка) — ссылка на изменяемый документ.ИмяРеквизита (Тип: Строка, длина 150) — имя изменившегося поля.НомерСтрокиТЧ (Тип: Число) — заполняется только при изменении строк табличной части.ИмяТабличнойЧасти (Тип: Строка, длина 100) — заполняется, если изменился реквизит ТЧ.Ресурсы:
ЗначениеБыло (Тип: ЛюбаяСсылка, Строка, Число, Дата, Булево).ЗначениеСтало (Тип: ЛюбаяСсылка, Строка, Число, Дата, Булево).Реквизиты:
Пользователь (Тип: СправочникСсылка.Пользователи или Строка).ДатаИзменения (Тип: Дата).Разберем по шагам алгоритм сравнения данных в модуле объекта документа. Нам необходимо перехватить событие ПередЗаписью, сравнить текущие реквизиты объекта с теми, что уже сохранены в базе данных, и записать различия.
Посмотрим на пример реализации кода в модуле документа:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Проверяем, что документ уже был записан ранее
Если ЭтоНовый() Тогда
Возврат;
КонецЕсли;
// Получаем состояние документа из базы данных до внесения текущих изменений
ДокументИзБазы = Ссылка.ПолучитьОбъект();
ТаблицаИзменений = Новый ТаблицаЗначений;
ТаблицаИзменений.Колонки.Добавить("ИмяРеквизита");
ТаблицаИзменений.Колонки.Добавить("ЗначениеБыло");
TaблицаИзменений.Колонки.Добавить("ЗначениеСтало");
// 1. Сравниваем основные реквизиты шапки документа
Для Каждого Реквизит Из Метаданные().Реквизиты Цикл
ИмяРеквизита = Реквизит.Имя;
ТекущееЗначение = ЭтотОбъект[ИмяРеквизита];
СтароеЗначение = ДокументИзБазы[ИмяРеквизита];
Если ТекущееЗначение <> СтароеЗначение Тогда
НоваяСтрока = ТаблицаИзменений.Добавить();
НоваяСтрока.ИмяРеквизита = ИмяРеквизита;
НоваяСтрока.ЗначениеБыло = СтароеЗначение;
НоваяСтрока.ЗначениеСтало = ТекущееЗначение;
КонецЕсли;
КонецЦикла;
// Если обнаружены изменения, записываем их в наш регистр
Если ТаблицаИзменений.Количество() > 0 Тогда
ЗаписатьЛогИзменений(ТаблицаИзменений);
КонецЕсли;
КонецПроцедуры
Процедура ЗаписатьЛогИзменений(ТаблицаИзменений)
Для Каждого СтрокаИзменения Из ТаблицаИзменений Цикл
НаборЗаписей = РегистрыСведений.ЖурналИзмененийДокументов.СоздатьНаборЗаписей();
// Устанавливаем отбор, чтобы не затереть другие логи
НаборЗаписей.Отбор.СсылкаНаОбъект.Установить(Ссылка);
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.СсылкаНаОбъект = Ссылка;
НоваяЗапись.ИмяРеквизита = СтрокаИзменения.ИмяРеквизита;
НоваяЗапись.ЗначениеБыло = СтрокаИзменения.ЗначениеБыло;
НоваяЗапись.ЗначениеСтало = СтрокаИзменения.ЗначениеСтало;
НоваяЗапись.Пользователь = ПараметрыСеанса.ТекущийПользователь;
НоваяЗапись.ДатаИзменения = ТекущаяДата();
// Записываем лог без замещения старых записей по другим реквизитам
НаборЗаписей.Записать(Ложь);
КонецЦикла;
КонецПроцедуры
---
Разберем ситуацию с табличными частями. Простое сравнение строк по индексу (строка 1, строка 2...) работать корректно не будет. Если пользователь удалит вторую строку из десяти, то все строки с третьей по десятую сместятся вверх. При обычном переборе индексов алгоритм решит, что изменились абсолютно все строки ниже удаленной.
Чтобы избежать этой проблемы, рекомендуется использовать скрытый реквизит табличной части (например, уникальный ключ строки типа УникальныйИдентификатор или системный КлючСвязи). При сравнении строк табличной части нужно сопоставлять данные именно по этому ключу, а не по физическому номеру строки в таблице.
---