Каждый разработчик 1С, работающий с типовыми или доработанными конфигурациями, рано или поздно сталкивается с подсистемой «Версионирование объектов» из Библиотеки стандартных подсистем (БСП). Этот механизм незаменим для аудита изменений: он позволяет увидеть, кто, когда и что изменил в документе или справочнике, а при необходимости — вернуться к предыдущему состоянию объекта. Для интерактивного отката правок конкретного пользователя можно задействовать готовое решение для отката изменений пользователя.
Однако в старых версиях БСП (и, соответственно, в таких конфигурациях, как УТ 11.1, УТ 10.3, КА 1.1, УПП 1.3) кроется серьезная архитектурная проблема. Стоит программисту добавить новый реквизит в документ или справочник, как вся накопленная история версий этого объекта становится «бомбой замедленного действия». При попытке восстановить любую версию, записанную до внесения изменений в метаданные, система выдает критическую ошибку и аварийно завершает процесс восстановления.
В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем поведение платформы и предложим проверенное практическое решение с готовым кодом для обхода этой проблемы.
Чтобы понять, где именно происходит сбой, давайте обратимся к программному коду БСП. Падение происходит на этапе десериализации сохраненной версии документа:
ЧтениеFastInfoSet.УстановитьДвоичныеДанные(ДвоичныеДанные);
Попытка
Объект = ПрочитатьXML(ЧтениеFastInfoSet);
Исключение
// Запись в журнал регистрации...
ТекстСообщенияОбОшибке = НСтр("ru = 'Не удалось перейти на выбранную версию...'");
КонецПопытки;
В данном процессе задействованы два ключевых звена, сочетание которых и приводит к ошибке:
FastInfoSet очень чувствителен к структуре сериализованных данных. Если метаданные объекта изменились, объект чтения теряет возможность корректно инициализировать поток для сопоставления старой структуры с новой конфигурацией.ПрочитатьXML() выполняет строгое сопоставление структуры XML-файла с текущим описанием объекта в конфигураторе. Если в конфигурации появился новый реквизит (которого, естественно, еще нет в старом XML-файле версии), то платформа ожидает его увидеть, не находит и выбрасывает системное исключение Ошибка преобразования данных XML. Восстановление становится невозможным.Давайте разберем по шагам, как можно обойти это платформенное ограничение и заставить систему «мягко» читать устаревшие версии объектов.
---
Для того чтобы обойти жесткое ограничение метода ПрочитатьXML(), нам потребуется модифицировать общий модуль, отвечающий за версионирование объектов (обычно это ВерсионированиеОбъектов или аналогичный в вашей конфигурации). Нам нужно реализовать двухэтапный алгоритм:
FastInfoSet (или ZIP-архив) в текстовый XML и выполнить ручной разбор тегов, сопоставляя только те реквизиты, которые реально присутствуют в сохраненной версии.Рассмотрим подробнее программную реализацию этого подхода.
Заменим штатную процедуру десериализации на устойчивую к изменениям метаданных функцию ВосстановитьОбъектПоXML. Этот код пытается выполнить стандартное чтение, а в случае неудачи — переходит к ручному разбору структуры XML:
Функция ВосстановитьОбъектПоXML(АдресВоВременномХранилище, Ссылка) Экспорт
УстановитьПривилегированныйРежим(Истина);
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресВоВременномХранилище);
ЧтениеFastInfoSet = Новый ЧтениеFastInfoSet;
Попытка
ЧтениеFastInfoSet.УстановитьДвоичныеДанные(ДвоичныеДанные);
ЧтениеFastInfoSet.Прочитать();
ЧтениеFastInfoSet.УстановитьДвоичныеДанные(ДвоичныеДанные);
Исключение
// Если FastInfoSet не смог прочитать двоичные данные напрямую,
// возможно, они упакованы в ZIP. Распакуем их во временный файл.
ИмяЗИП = КаталогВременныхФайлов() + Лев(Новый УникальныйИдентификатор(), 8) + ".zip";
ДвоичныеДанные.Записать(ИмяЗИП);
ФайлАрхива = Новый ЧтениеZIPФайла(ИмяЗИП);
ИмяФайлаXML = КаталогВременныхФайлов() + ФайлАрхива.Элементы[0].ПолноеИмя;
ФайлАрхива.Извлечь(ФайлАрхива.Элементы[0], КаталогВременныхФайлов(), РежимВосстановленияПутейФайловZIP.НеВосстанавливать);
ФайлАрхива.Закрыть();
Текст = Новый ТекстовыйДокумент;
Текст.Прочитать(ИмяФайлаXML);
ЧтениеFastInfoSet = Новый ЧтениеXML;
ЧтениеFastInfoSet.УстановитьСтроку(Текст.ПолучитьТекст());
Текст = "";
УдалитьФайлы(ИмяФайлаXML);
УдалитьФайлы(ИмяЗИП);
КонецПопытки;
ФлагИсключение = Ложь;
Попытка
// Пробуем стандартное платформенное чтение
Объект = ПрочитатьXML(ЧтениеFastInfoSet);
Исключение
// Если упали — взводим флаг для запуска ручного парсинга
ФлагИсключение = Истина;
КонецПопытки;
Если ФлагИсключение Тогда
ФлагИсключение = Ложь;
Попытка
// Запускаем ручной парсер, который вернет структуру прочитанных данных
Результат = РазборПредставленияОбъектаXML(ДвоичныеДанные, Ссылка);
Объект = Ссылка.ПолучитьОбъект();
// Заполняем реквизиты шапки документа/справочника
Для Каждого Реквизит Из Результат.Реквизиты Цикл
ИмяРеквизита = Реквизит.НаименованиеРеквизита;
// Пропускаем системные реквизиты, управляемые платформой
Если ИмяРеквизита = "Ref" Или ИмяРеквизита = "UUID" Тогда
Продолжить;
КонецЕсли;
// Отрабатываем стандартные реквизиты
Если ИмяРеквизита = "DeletionMark" Тогда
Объект.ПометкаУдаления = Реквизит.ЗначениеРеквизита;
Продолжить;
КонецЕсли;
Если ИмяРеквизита = "Date" Тогда
Объект.Дата = Реквизит.ЗначениеРеквизита;
Продолжить;
КонецЕсли;
Если ИмяРеквизита = "Number" Тогда
Объект.Номер = Реквизит.ЗначениеРеквизита;
Продолжить;
КонецЕсли;
Если ИмяРеквизита = "Posted" Тогда
Объект.Проведен = Реквизит.ЗначениеРеквизита;
Продолжить;
КонецЕсли;
// Заполняем только те реквизиты, которые реально существуют в текущих метаданных
Если Объект.Метаданные().Реквизиты.Найти(ИмяРеквизита) <> Неопределено Тогда
Объект[ИмяРеквизита] = Реквизит.ЗначениеРеквизита;
КонецЕсли;
КонецЦикла;
// Заполняем табличные части
Для Каждого ТабЧастьСоответствие Из Результат.ТабличныеЧасти Цикл
ИмяТЧ = ТабЧастьСоответствие.Ключ;
ТЗ = ТабЧастьСоответствие.Значение;
Если ТЗ <> Неопределено И ТЗ.Количество() > 0 Тогда
// Проверяем, существует ли табличная часть в текущих метаданных объекта
Если ОбщегоНазначения.ЕстьТабЧастьДокумента(ИмяТЧ, Объект.Метаданные()) Тогда
Объект[ИмяТЧ].Очистить();
Для Каждого СтрокаТЗ Из ТЗ Цикл
НоваяСтрока = Объект[ИмяТЧ].Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЗ);
КонецЦикла;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Исключение
ФлагИсключение = Истина;
КонецПопытки;
КонецЕсли;
Если ФлагИсключение Тогда
ТекстСообщенияОбОшибке = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
ЗаписьЖурналаРегистрации("Версионирование.ОшибкаВосстановления", УровеньЖурналаРегистрации.Ошибка,,, ТекстСообщенияОбОшибке);
Возврат Неопределено;
КонецЕсли;
Возврат Объект;
КонецФункции
Для разбора структуры XML воспользуемся последовательным чтением через ЧтениеXML. Наша задача — обойти дерево XML-документа, собрать имена реквизитов и их значения, а также воссоздать строки табличных частей в виде таблиц значений. Такой подход полностью застрахует нас от ошибок преобразования типов при изменении состава метаданных.
Функция РазборПредставленияОбъектаXML(ДвоичныеДанные, Ссылка)
// Структура для хранения результатов парсинга
Результат = Новый Структура;
ТаблицаРеквизитов = Новый ТаблицаЗначений;
ТаблицаРеквизитов.Колонки.Добавить("НаименованиеРеквизита", Новый ОписаниеТипов("Строка"));
ТаблицаРеквизитов.Колонки.Добавить("ЗначениеРеквизита");
Результат.Вставить("Реквизиты", ТаблицаРеквизитов);
Результат.Вставить("ТабличныеЧасти", Новый Соответствие);
ЧтениеXML = Новый ЧтениеXML;
// Пытаемся инициализировать чтение двоичных данных
Попытка
ЧтениеXML.УстановитьДвоичныеДанные(ДвоичныеДанные);
Исключение
// Если это сжатый ZIP, распакуем аналогично первому шагу
ИмяЗИП = КаталогВременныхФайлов() + Лев(Новый УникальныйИдентификатор(), 8) + ".zip";
ДвоичныеДанные.Записать(ИмяЗИП);
ФайлАрхива = Новый ЧтениеZIPФайла(ИмяЗИП);
ИмяФайлаXML = КаталогВременныхФайлов() + ФайлАрхива.Элементы[0].ПолноеИмя;
ФайлАрхива.Извлечь(ФайлАрхива.Элементы[0], КаталогВременныхФайлов(), РежимВосстановленияПутейФайловZIP.НеВосстанавливать);
ФайлАрхива.Закрыть();
Текст = Новый ТекстовыйДокумент;
Текст.Прочитать(ИмяФайлаXML);
ЧтениеXML.УстановитьСтроку(Текст.ПолучитьТекст());
УдалитьФайлы(ИмяФайлаXML);
УдалитьФайлы(ИмяЗИП);
КонецПопытки;
// Запускаем цикл последовательного чтения XML
ТекущаяТабличнаяЧасть = "";
ТекущаяСтрокаТЧ = Неопределено;
ТаблицаТЧ = Неопределено;
Пока ЧтениеXML.Прочитать() Цикл
ТипУзла = ЧтениеXML.ТипУзла;
Если ТипУзла = ТипУзлаЧтенияXML.НачалоЭлемента Тогда
ИмяЭлемента = ЧтениеXML.Имя;
// Проверяем, не является ли элемент объявлением табличной части
Если Найти(ИмяЭлемента, "TabularSection.") > 0 Или Найти(ИмяЭлемента, "ТабличнаяЧасть.") > 0 Тогда
ТекущаяТабличнаяЧасть = Сред(ИмяЭлемента, Найти(ИмяЭлемента, ".") + 1);
ТаблицаТЧ = Новый ТаблицаЗначений;
Результат.ТабличныеЧасти.Вставить(ТекущаяТабличнаяЧасть, ТаблицаТЧ);
Продолжить;
КонецЕсли;
// Если мы внутри табличной части и встретили строку (обычно элемент "Row" или "Строка")
Если ЗначениеЗаполнено(ТекущаяТабличнаяЧасть) И (ИмяЭлемента = "Row" Или ИмяЭлемента = "Строка") Тогда
ТекущаяСтрокаТЧ = ТаблицаТЧ.Добавить();
Продолжить;
КонецЕсли;
// Чтение значения реквизита шапки или поля табличной части
Если ЧтениеXML.Прочитать() И ЧтениеXML.ТипУзла = ТипУзлаЧтенияXML.Текст Тогда
ЗначениеТекста = ЧтениеXML.Значение;
Если ЗначениеЗаполнено(ТекущаяТабличнаяЧасть) Тогда
// Добавляем колонку в ТЗ табличной части, если ее еще нет
Если ТаблицаТЧ.Колонки.Найти(ИмяЭлемента) = Неопределено Тогда
ТаблицаТЧ.Колонки.Добавить(ИмяЭлемента);
КонецЕсли;
ТекущаяСтрокаТЧ[ИмяЭлемента] = ЗначениеТекста;
Else
// Реквизит шапки
НовыйРеквизит = ТаблицаРеквизитов.Добавить();
НовыйРеквизит.НаименованиеРеквизита = ИмяЭлемента;
НовыйРеквизит.ЗначениеРеквизита = ЗначениеТекста;
КонецЕсли;
КонецЕсли;
ИначеЕсли ТипУзла = ТипУзлаЧтенияXML.КонецЭлемента Тогда
ИмяЭлемента = ЧтениеXML.Имя;
// Если вышли из табличной части, сбрасываем контекст
Если Найти(ИмяЭлемента, "TabularSection.") > 0 Или Найти(ИмяЭлемента, "ТабличнаяЧасть.") > 0 Тогда
ТекущаяТабличнаяЧасть = "";
ТаблицаТЧ = Неопределено;
КонецЕсли;
КонецЕсли;
КонецЦикла;
ЧтениеXML.Закрыть();
Возврат Результат;
КонецФункции
Важное примечание по типам данных: Представленный выше алгоритм парсинга считывает значения как строки. Для полноценного восстановления ссылочных типов, чисел и дат в продакшн-решениях рекомендуется использовать типизацию данных (например, преобразовывать строки XML к типам метаданных через XMLЗначение(Тип, СтроковоеЗначение)). Платформа 1С умеет автоматически восстанавливать значения простых типов и ссылок, если передать ей целевой тип реквизита из метаданных.
---
Если вы работаете на современной версии платформы 1С:Предприятие (начиная с релиза 8.3.11) и используете актуальные версии БСП, у вас есть гораздо более изящные пути решения этой проблемы, не требующие написания сложных ручных парсеров.
В новых версиях платформы фирма «1С» реализовала встроенный механизм версионирования — «История данных». В отличие от старого БСП-версионирования, он имеет ряд критических преимуществ:
ИсторияДанных.СформироватьПоВерсии() платформа самостоятельно сопоставит старую и новую схему. Если появились новые реквизиты, они просто останутся пустыми, не вызывая ошибок сбоя десериализации.НастройкаИПереносИсторииДанных.epf, которая позволяет перенести накопленные версии из регистра сведений ВерсииОбъектов напрямую в системную «Историю данных».Если вы хотите оставить старую подсистему версионирования, но переписать ее более технологично, откажитесь от прямого вызова ПрочитатьXML. Задействуйте Фабрику XDTO:
Если вы разрабатываете собственную подсистему версионирования или глубоко кастомизируете существующую, рассмотрите возможность хранения версий в формате JSON вместо XML:
// Сериализация объекта в JSON-строку
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, Объект);
JSONСтрока = ЗаписьJSON.Закрыть();
Десериализация JSON-строки в соответствие или структуру никогда не вызовет ошибку при добавлении новых реквизитов в метаданные. Недостающие свойства просто не будут заполнены, что позволит вам безопасно применить метод ЗаполнитьЗначенияСвойств(Объект, СтруктураИзJSON).
---
Падение версионирования из-за изменения метаданных — классическая проблема старых релизов 1С. Мы проанализировали причины сбоя формата FastInfoSet и предложили надежный обходной путь с ручным парсингом структуры XML. Для устаревших конфигураций (УТ 10.3 / УТ 11.1) данный код является единственным способом спасти накопленную историю изменений. Если же вы работаете на современных версиях платформы — рассмотрите возможность планомерного перехода на встроенную «Историю данных». При необходимости обратного переноса данных из-за ограничений свёртки можно применить инструмент перехода с механизма История данных на Версионирование объектов БСП.