При разработке систем на платформе 1С часто возникает задача отслеживания того, как менялись значения определенных полей справочника во времени. Это критично для построения отчетов «на дату», аудита действий пользователей или восстановления ошибочно измененных данных. Рассмотрим подробно, как реализовать хранение истории для двух и более реквизитов, проанализируем архитектурные подходы и выберем наиболее оптимальный вариант.
Начиная с версии платформы 8.3.11, разработчикам доступен мощный встроенный инструмент — История данных. Прежде чем создавать собственные объекты метаданных, проанализируем возможности этого механизма. Он позволяет автоматически логировать изменения объектов практически без написания кода.
Для активации этого способа выполним следующие шаги:
Использовать.История данных (обычно установлено Использовать по умолчанию).Плюсы этого метода: не нужно создавать регистры, история пишется транзакционно самой платформой, есть встроенные методы для программного чтения данных (МенеджерИсторииДанных). Однако, если справочник очень большой и изменения происходят постоянно, этот механизм может создать дополнительную нагрузку на базу данных. Кроме того, создание сложных аналитических отчетов по этой истории может быть затруднительным по сравнению с обычным запросом к регистру.
Если ваша конфигурация создана на базе Библиотеки стандартных подсистем (БСП), мы рекомендуем использовать подсистему «Версионирование объектов». Это «золотой стандарт» для типовых конфигураций (ERP, КА, УТ 11, БП 3.0).
В этом случае нам вообще не нужно лезть в конфигуратор. В пользовательском режиме (Администрирование — Общие настройки) достаточно включить версионирование для нужного справочника. Система будет сохранять полные копии объекта в специальном регистре в формате XML. Это очень удобно для визуального сравнения версий «было / стало» через типовой отчет, но не всегда подходит, если нужно быстро получать значение конкретного реквизита программно в запросах.
Если штатные механизмы по каким-то причинам не подходят (например, нужна высокая производительность или специфические отчеты), разберем, как спроектировать собственный регистр. Один из главных вопросов обсуждения: нужно ли делать два отдельных регистра для двух реквизитов?
Проанализируем ситуацию: если мы создадим один регистр с колонками Реквизит1 и Реквизит2, то при изменении только одного из них нам придется либо записывать оба (дублируя старое значение второго), либо оставлять второе пустым, что усложнит получение актуального состояния через СрезПоследних. Поэтому наиболее гибкой архитектурой будет использование одного регистра, но с измерением ИмяРеквизита.
Рекомендуемая структура регистра:
В пределах секунды (или позиции регистратора).Объект: Тип — ссылка на ваш справочник. Важно: Установите свойство Ведущее, чтобы при удалении элемента история очищалась автоматически.ИмяРеквизита: Тип Строка (например, длина 50) или Перечисление.Значение: Составной тип, включающий типы всех отслеживаемых реквизитов.АвторИзменений: Ссылка на справочник Пользователи.Такая структура позволяет хранить историю любого количества полей в одной таблице, не раздувая её лишними колонками и записями.
Разберем, как правильно реализовать запись изменений. Нам нужно сравнить старые значения с новыми. Лучше всего делать это в модуле объекта справочника. Рассмотрим алгоритм действий:
1. В процедуре ПередЗаписью мы должны выяснить, изменились ли интересующие нас поля. Для этого нужно прочитать текущие данные из базы, так как в ЭтотОбъект уже находятся новые значения, пришедшие с формы.
2. Чтобы не записывать данные, если запись объекта не произойдет (например, из-за отказа в проверке), мы передадим список изменений в процедуру ПриЗаписи через ДополнительныеСвойства.
Посмотрим на пример кода в модуле справочника:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Если это новый объект, истории еще нет, но можно записать начальные значения
Если ЭтоНовый() Тогда
Возврат;
КонецЕсли;
// Получаем старые значения из базы данных
СтарыеЗначения = Ссылка.ПолучитьОбъект();
СписокИзменений = Новый Структура;
Если ЭтотОбъект.Реквизит1 <> СтарыеЗначения.Реквизит1 Тогда
СписокИзменений.Вставить("Реквизит1", ЭтотОбъект.Реквизит1);
КонецЕсли;
Если ЭтотОбъект.Реквизит2 <> СтарыеЗначения.Реквизит2 Тогда
СписокИзменений.Вставить("Реквизит2", ЭтотОбъект.Реквизит2);
КонецЕсли;
// Сохраняем информацию для процедуры ПриЗаписи
ДополнительныеСвойства.Вставить("ИсторияДляЗаписи", СписокИзменений);
КонецПроцедуры
Процедура ПриЗаписи(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
Если ДополнительныеСвойства.Свойство("ИсторияДляЗаписи") Тогда
СписокИзменений = ДополнительныеСвойства.ИсторияДляЗаписи;
Для Каждого Изменение Из СписокИзменений Цикл
МенеджерЗаписи = РегистрыСведений.ИсторияРеквизитов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = ТекущаяДатаСеанса();
МенеджерЗаписи.Объект = Ссылка;
МенеджерЗаписи.ИмяРеквизита = Изменение.Ключ;
МенеджерЗаписи.Значение = Изменение.Значение;
МенеджерЗаписи.АвторИзменений = Пользователи.ТекущийПользователь();
МенеджерЗаписи.Записать();
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Проанализируем ситуацию, когда изменений очень много. В приведенном выше коде используется ПолучитьОбъект() в процедуре ПередЗаписью. Это создает дополнительный запрос к базе данных. В высоконагруженных системах вместо ПолучитьОбъект() лучше использовать облегченный запрос через Запрос, выбирающий только нужные поля по ссылке. Это значительно сэкономит ресурсы сервера.
Также важно помнить о свойстве ОбменДанными.Загрузка. Если ваша база участвует в обмене (РИБ или синхронизация), то при загрузке данных из других узлов история может дублироваться или записываться некорректно. Всегда добавляйте проверку этого флага в начало процедур.
Выясним, как эффективно извлекать эти данные для отчетов. Если мы использовали вариант с регистром и измерением ИмяРеквизита, то получить значение на любую дату можно простым запросом к виртуальной таблице СрезПоследних:
ВЫБРАТЬ
История.Значение КАК ЗначениеРеквизита1
ИЗ
РегистрСведений.ИсторияРеквизитов.СрезПоследних(&НужнаяДата, Объект = &НашОбъект И ИмяРеквизита = "Реквизит1") КАК История
Этот подход гораздо быстрее и надежнее, чем хранение истории в табличных частях самого справочника. Хотя хранение в ТЧ (как это сделано, например, для КПП контрагентов в некоторых версиях ERP) допустимо, оно сильно усложняет получение данных в разрезе множества объектов, так как требует обращения к основной таблице справочника и его вложенным таблицам.
Подведем итог: для большинства задач оптимальным решением является использование штатного механизма История данных. Если же вам нужен полный контроль над структурой и скоростью формирования отчетов — выбирайте один периодический регистр сведений с гибкой структурой измерений.