В процессе эксплуатации информационных систем на базе «1С:Предприятие» пользователи и администраторы регулярно сталкиваются с ситуацией, когда данные в справочниках или документах были изменены некорректно. Если в базе данных включена подсистема «Версионирование объектов» из состава Библиотеки стандартных подсистем (БСП), у нас есть возможность посмотреть историю изменений и при необходимости вернуться к предыдущему состоянию данных.
Однако на практике часто возникают трудности. Во-первых, нужная кнопка перехода к истории версий может быть скрыта в интерфейсе (например, в конфигурации «Управление торговлей 11»). Во-вторых, попытки программно прочитать сохраненную версию объекта из информационной базы напрямую через XML-парсер часто заканчиваются фатальными ошибками разбора данных. В этой статье мы детально разберем обе проблемы, выясним причины их возникновения и найдем эффективные способы решения как через пользовательский интерфейс, так и с помощью программного кода.
Прежде чем приступать к написанию обработок, проанализируем штатные возможности конфигурации. В типовых решениях (УТ 11, КА 2, ERP) для просмотра истории изменений используется специальная команда, которая открывает отчет по версиям объекта. Но часто пользователи не могут найти эту кнопку на форме элемента справочника (например, «Номенклатура»).
Выясним причину этого поведения. В некоторых релизах конфигураций видимость команды «История изменений» для справочника номенклатуры может быть отключена на уровне метаданных или свойств формы. Чтобы вернуть ее на форму, выполним следующие шаги:
Номенклатура.ИсторияИзменений (в некоторых случаях она вынесена в общую командную панель формы). Если флаг видимости снят, установим его в значение Истина.После выполнения этих действий в режиме «1С:Предприятие» на форме элемента справочника в меню «Еще» или непосредственно на панели навигации появится долгожданная ссылка. При переходе по ней откроется отчет, где можно сравнить любые две версии и нажать кнопку «Перейти на версию» для интерактивного отката изменений.
Если перед нами стоит задача выполнить групповой или пакетный откат множества объектов (например, вернуть всю номенклатуру к состоянию «Версии №2»), интерактивный метод не подойдет. Требуется написать программный скрипт.
Часто разработчики пытаются получить данные версии напрямую из регистра сведений ВерсииОбъектов. Типичный неудачный код выглядит следующим образом:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
| ВерсииОбъектов.ВерсияОбъекта КАК Хранилище
|ИЗ
| РегистрСведений.ВерсииОбъектов КАК ВерсииОбъектов
|ГДЕ
| ВерсииОбъектов.Объект = &Объект";
// ... Получение двоичных данных и попытка прочитать их через ЧтениеXML
ДвоичныеДанные = ХранилищеОбъекта.Получить();
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(ИмяФайла);
Объект = ПрочитатьXML(ЧтениеXML); // Здесь происходит падение программы!
При выполнении этого кода платформа генерирует ошибку разбора XML: «Document is empty» или указывает на недопустимые символы в первой строке файла. Давайте разберем причину этого сбоя.
Дело в том, что БСП в целях оптимизации дискового пространства и ускорения сериализации сохраняет данные версий в формате FastInfoset (двоичный XML). Этот формат не является стандартным текстовым XML. Он оптимизирован для сжатия и быстрого разбора платформой. Попытка прочитать двоичные данные FastInfoset с помощью стандартного объекта ЧтениеXML обречена на провал, поскольку парсер ожидает увидеть текстовую разметку (теги, угловые скобки), а получает бинарный поток.
Для корректной десериализации таких данных в платформе «1С:Предприятие» предусмотрен специализированный класс ЧтениеFastInfoSet. Именно его необходимо использовать для разбора двоичных данных из хранилища версий.
Рассмотрим правильный алгоритм десериализации объекта из истории версий БСП. Для этого нам понадобятся двоичные данные из регистра сведений и объект ЧтениеFastInfoSet.
Ниже представлен рабочий пример кода, который извлекает сохраненную версию объекта и восстанавливает ее в базу данных:
// Определим ссылку на объект и номер версии, которую хотим восстановить
СсылкаНаОбъект = Справочники.Номенклатура.НайтиПоКоду("00000001");
НужныйНомерВерсии = 2;
// Выберем данные версии из регистра сведений ВерсииОбъектов
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВерсииОбъектов.ВерсияОбъекта КАК ХранилищеВерсии
|ИЗ
| РегистрСведений.ВерсииОбъектов КАК ВерсииОбъектов
|ГДЕ
| ВерсииОбъектов.Объект = &Объект
| И ВерсииОбъектов.НомерВерсии = &НомерВерсии";
Запрос.УстановитьПараметр("Объект", СсылкаНаОбъект);
Запрос.УстановитьПараметр("НомерВерсии", НужныйНомерВерсии);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
// Получаем двоичные данные из ХранилищаЗначения
ДвоичныеДанныеВерсии = Выборка.ХранилищеВерсии.Получить();
Если ДвоичныеДанныеВерсии <> Неопределено Тогда
// Используем ЧтениеFastInfoSet для работы с бинарным XML
ЧтениеFIS = Новый ЧтениеFastInfoSet;
ЧтениеFIS.УстановитьДвоичныеДанные(ДвоичныеДанныеВерсии);
// Восстанавливаем объект из XML-данных
ВосстановленныйОбъект = ПрочитатьXML(ЧтениеFIS);
ЧтениеFIS.Закрыть();
// Записываем объект в базу данных
Если ВосстановленныйОбъект <> Неопределено Тогда
ВосстановленныйОбъект.ОбменДанными.Загрузка = Истина; // Отключаем лишние подписки и проверки
ВосстановленныйОбъект.Записать();
Сообщить("Объект " + СсылкаНаОбъект + " успешно восстановлен до версии " + НужныйНомерВерсии);
КонецЕсли;
КонецЕсли;
Иначе
Сообщить("Указанная версия не найдена в регистре версий объектов!", СтатусСообщения.Внимание);
КонецЕсли;
Теперь решим более сложную и масштабную задачу: автоматизируем процесс массового отката всей номенклатуры в базе данных до конкретной версии (в нашем примере — до версии под номером 2). При написании таких обработок крайне важно оборачивать каждую операцию записи в транзакцию и перехватывать возможные исключения, чтобы не нарушить целостность базы данных при возникновении непредвиденных ошибок.
Проанализируем готовое пакетное решение:
Процедура ВосстановитьВсюНоменклатуруДоВерсии2() Экспорт
// Выберем все объекты справочника Номенклатура, у которых есть сохраненная версия №2
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВерсииОбъектов.Объект КАК Ссылка,
| ВерсииОбъектов.ВерсияОбъекта КАК ХранилищеВерсии
|ИЗ
| РегистрСведений.ВерсииОбъектов КАК ВерсииОбъектов
|ГДЕ
| ВерсииОбъектов.Объект ССЫЛКА Справочник.Номенклатура
| И ВерсииОбъектов.НомерВерсии = 2";
Выборка = Запрос.Выполнить().Выбрать();
УспешноВосстановлено = 0;
ОшибокПриВосстановлении = 0;
Пока Выборка.Следующий() Цикл
НачалоТранзакции();
Попытка
ДвоичныеДанныеВерсии = Выборка.ХранилищеВерсии.Получить();
Если ДвоичныеДанныеВерсии = Неопределено Тогда
ВызватьИсключение "Пустые двоичные данные в хранилище версии.";
КонецЕсли;
// Чтение бинарного XML FastInfoset
ЧтениеFIS = Новый ЧтениеFastInfoSet;
ЧтениеFIS.УстановитьДвоичныеДанные(ДвоичныеДанныеВерсии);
ВосстановленныйОбъект = ПрочитатьXML(ЧтениеFIS);
ЧтениеFIS.Закрыть();
Если ВосстановленныйОбъект = Неопределено Тогда
ВызватьИсключение "Не удалось десериализовать объект из XML.";
КонецЕсли;
// Устанавливаем режим загрузки, чтобы обойти прикладные проверки при записи
ВосстановленныйОбъект.ОбменДанными.Загрузка = Истина;
ВосстановленныйОбъект.Записать();
ЗафиксироватьТранзакцию();
УспешноВосстановлено = УспешноВосстановлено + 1;
Исключение
ОтменитьТранзакцию();
ОшибокПриВосстановлении = ОшибокПриВосстановлении + 1;
// Логируем ошибку для последующего анализа разработчиком
ЗаписьЖурналаРегистрации(
"ВосстановлениеВерсий",
УровеньЖурналаРегистрации.Ошибка,
Справочники.Номенклатура,
Выборка.Ссылка,
"Ошибка при восстановлении версии 2 для объекта: " + Выборка.Ссылка +
". Описание ошибки: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке())
);
КонецПопытки;
КонецЦикла;
Сообщить(СтрШаблон("Обработка завершена. Успешно восстановлено: %1, Ошибок (см. Журнал регистрации): %2",
УспешноВосстановлено, ОшибокПриВосстановлении));
КонецПроцедуры
Для более чистого программирования в среде БСП рекомендуется по возможности избегать прямых запросов к системным регистрам сведений и использовать официальный программный интерфейс (API) библиотек. В общем модуле ВерсионированиеОбъектов реализованы полезные функции, которые берут на себя рутину по работе с FastInfoset и временными хранилищами данных.
Например, мы можем извлечь восстановленный объект с помощью метода БСП ВосстановитьОбъектПоXML:
// Сначала получаем структуру сведений о версии
СведенияОВерсии = ВерсионированиеОбъектов.СведенияОВерсииОбъекта(СсылкаНаОбъект, 2);
Если СведенияОВерсии.ВерсияОбъекта <> Неопределено Тогда
// Помещаем двоичные данные во временное хранилище
АдресХранилища = ПоместитьВоВременноеХранилище(СведенияОВерсии.ВерсияОбъекта);
ТекстОшибки = "";
// БСП сама распакует FastInfoset и вернет заполненный объект нужного типа
ВосстановленныйОбъект = ВерсионированиеОбъектов.ВосстановитьОбъектПоXML(АдресХранилища, ТекстОшибки);
Если ВосстановленныйОбъект <> Неопределено И ПустаяСтрока(ТекстОшибки) Тогда
ВосстановленныйОбъект.ОбменДанными.Загрузка = Истина;
ВосстановленныйОбъект.Записать();
Иначе
Сообщить("Ошибка БСП при восстановлении: " + ТекстОшибки);
EndIf;
КонецЕсли;
Также важно упомянуть о современном механизме платформы «1С:Предприятие 8.3» — «История данных». Этот механизм работает непосредственно на уровне ядра СУБД и платформы, не нагружая базу данных дополнительными таблицами регистров БСП. Если в вашей конфигурации включена именно платформенная история данных, то получить объект любой версии можно буквально в три строки кода с помощью менеджера ИсторияДанных:
// Пример получения объекта по версии через платформенную Историю Данных
НомерВерсииДанных = 2;
ОбъектНоменклатуры = ИсторияДанных.СформироватьПоВерсии(СсылкаНаОбъект, НомерВерсииДанных);
ОбъектНоменклатуры.Записать();
При использовании любого из описанных программных методов (будь то прямой разбор FastInfoset, методы БСП или история данных) разработчики могут столкнуться со следующей серьезной проблемой: изменение структуры метаданных.
Представим ситуацию: версия объекта №2 была создана год назад. С тех пор конфигурация обновилась, разработчики добавили новые реквизиты в справочник номенклатуры, а некоторые старые удалили или изменили их тип данных. При попытке десериализации старого двоичного XML платформа выдаст ошибку, так как структура XML-файла не соответствует текущему описанию справочника в метаданных вашей системы.
В этом случае стандартное восстановление «в лоб» не сработает. Придется использовать объект ЧтениеFastInfoSet для разбора данных в дерево значений или структуру соответствий, программно дополнять или вырезать неактуальные узлы данных, а затем вручную переносить реквизиты в новый пустой объект справочника.