При интеграции систем, миграции данных или сверке распределенных баз данных программисты 1С часто сталкиваются с необходимостью сравнения объектов. Ситуация значительно усложняется, если конфигурации двух баз имеют различия в структуре метаданных. Например, в Базе №1 документ содержит табличные части Состав1 и Состав2, а в Базе №2 этот же документ содержит только табличную часть Состав1. Наша задача — сравнить шапки документов и общую для обеих баз табличную часть Состав1, полностью проигнорировав изменения в Состав2.
В этой статье мы подробно разберем, как решить эту задачу оптимальными способами, проанализируем скрытые технические проблемы, с которыми можно столкнуться при наивной реализации, и предоставим готовые шаблоны кода.
На первый взгляд идея кажется простой: сериализовать оба документа в JSON, получить хэш-строку (например, MD5) и сравнить их. Однако при детальном рассмотрении этот подход наталкивается на три серьезные проблемы:
Очистить() для удаления содержимого Состав2 перед сериализацией в Базе №1, табличная часть просто станет пустой. Сам узел ТЧ не исчезнет из метаданных. При сериализации через СериализаторXDTO в JSON все равно запишется пустое свойство: "Состав2": []. В Базе №2 этого свойства не будет вообще. Как результат — хэши файлов гарантированно разойдутся, даже если все остальные данные идентичны.СериализаторXDTO в JSON-строку записываются служебные символы, например, "#type" для обозначения типа данных. Символ решетки # является недопустимым в именах ключей для объекта Структура в 1С. Попытка десериализовать такой JSON напрямую в структуру вызовет критическую ошибку. Чтение необходимо выполнять строго в объект типа Соответствие.Самый надежный и контролируемый способ — это ручная сборка "чистой" структуры данных на основе метаданных документа. Мы можем написать универсальную функцию, которая обойдет все реквизиты шапки и нужные табличные части, пропустив исключенные объекты. Этот метод также решает проблему с UUID, позволяя автоматически заменять ссылки их кодами.
Рассмотрим подробный пример реализации такой функции:
// Функция собирает структуру только из нужных реквизитов и табличных частей
Функция ПолучитьДанныеДляСравнения(ДокументСсылка, ИсключаемыеРеквизиты = Неопределено, ИсключаемыеТЧ = Неопределено) Экспорт
Если ИсключаемыеРеквизиты = Неопределено Тогда
ИсключаемыеРеквизиты = Новый Массив;
КонецЕсли;
Если ИсключаемыеТЧ = Неопределено Тогда
ИсключаемыеТЧ = Новый Массив;
КонецЕсли;
Объект = ДокументСсылка.ПолучитьОбъект();
МетаданныеОбъекта = Объект.Метаданные();
Результат = Новый Структура;
// 1. Обработка реквизитов шапки документа
Для Каждого Реквизит Из МетаданныеОбъекта.Реквизиты Цикл
ИмяРеквизита = Реквизит.Имя;
Если ИсключаемыеРеквизиты.Найти(ИмяРеквизита) <> Неопределено Тогда
Продолжить;
КонецЕсли;
Значение = Объект[ИмяРеквизита];
// Заменяем ссылки на коды для избежания конфликтов UUID в разных базах
Если Справочники.ТипВсеСсылки().СодержитТип(ТипЗнч(Значение)) И Значение <> Неопределено Тогда
ЗначениеСравнения = Значение.Код;
ИначеЕсли Документы.ТипВсеСсылки().СодержитТип(ТипЗнч(Значение)) И Значение <> Неопределено Тогда
ЗначениеСравнения = Значение.Номер; // Для документов используем номер
Иначе
ЗначениеСравнения = Значение;
КонецЕсли;
Результат.Вставить(ИмяРеквизита, ЗначениеСравнения);
КонецЦикла;
// 2. Обработка табличных частей
Для Каждого ТЧ Из МетаданныеОбъекта.ТабличныеЧасти Цикл
ИмяТЧ = ТЧ.Имя;
Если ИсключаемыеТЧ.Найти(ИмяТЧ) <> Неопределено Тогда
Продолжить;
КонецЕсли;
МассивСтрок = Новый Массив;
Для Каждого СтрокаТЧ Из Объект[ИмяТЧ] Цикл
СтруктураСтроки = Новый Структура;
Для Каждого РеквизитТЧ Из ТЧ.Реквизиты Цикл
ИмяРеквизитаТЧ = РеквизитТЧ.Имя;
ЗначениеТЧ = СтрокаТЧ[ИмяРеквизитаТЧ];
// Заменяем ссылки в табличной части
Если Справочники.ТипВсеСсылки().СодержитТип(ТипЗнч(ЗначениеТЧ)) И ЗначениеТЧ <> Неопределено Тогда
ЗначениеСравнения = ЗначениеТЧ.Код;
Иначе
ЗначениеСравнения = ЗначениеТЧ;
КонецЕсли;
СтруктураСтроки.Вставить(ИмяРеквизитаТЧ, ЗначениеСравнения);
КонецЦикла;
МассивСтрок.Добавить(СтруктураСтроки);
КонецЦикла;
Результат.Вставить(ИмяТЧ, МассивСтрок);
КонецЦикла;
Возврат Результат;
КонецФункции
Проанализируем преимущества данного подхода. Во-первых, мы полностью контролируем состав полей. Во-вторых, мы привели все ссылки к кодам (или номерам), что гарантирует идентичность данных при сравнении в разных базах. Теперь нам остается лишь преобразовать полученную структуру в строку JSON:
// Подготовка параметров исключения
ИсключаемыеТЧ = Новый Массив;
ИсключаемыеТЧ.Добавить("Состав2"); // Данную ТЧ мы игнорируем
// Получаем "чистые" данные документа
ДанныеДокумента = ПолучитьДанныеДляСравнения(Выборка.Ссылка, , ИсключаемыеТЧ);
// Сериализуем структуру в JSON-строку
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ДанныеДокумента);
СтрокаСравнения = ЗаписьJSON.Закрыть();
Эту строку можно легко передать по веб-сервису, сохранить в файл или преобразовать в хэш-сумму (например, через объект ХешированиеДанных) для мгновенного сравнения с аналогичной строкой из второй базы.
Если вы хотите избежать ручного перебора метаданных и предпочитаете использовать стандартный механизм СериализаторXDTO, то вам потребуется промежуточная десериализация. Смысл метода заключается в том, чтобы выгрузить документ целиком, прочитать его в объект типа Соответствие, удалить ненужные ключи на программном уровне и сериализовать обратно.
Посмотрим на пример реализации данного алгоритма:
// Шаг 1. Сериализуем объект целиком через XDTO-сериализатор
ОбъектДокумента = Выборка.Ссылка.ПолучитьОбъект();
ЗаписьJson = Новый ЗаписьJSON;
ЗаписьJson.УстановитьСтроку();
СериализаторXDTO.ЗаписатьJSON(ЗаписьJson, ОбъектДокумента, НазначениеТипаXML.Явное);
ИсходныйJSON = ЗаписьJson.Закрыть();
// Шаг 2. Десериализуем JSON-строку в Соответствие (параметр Истина обязателен!)
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(ИсходныйJSON);
ДанныеОбъекта = ПрочитатьJSON(ЧтениеJSON, Истина);
ЧтениеJSON.Закрыть();
// Шаг 3. Удаляем ненужные узлы
// Удаляем табличную часть, которой нет во второй базе
ДанныеОбъекта.Удалить("Состав2");
// Также рекомендуется удалить системные свойства, которые всегда различаются между базами
ДанныеОбъекта.Удалить("ВерсияДанных");
ДанныеОбъекта.Удалить("Ссылка");
// Шаг 4. Сериализуем очищенные данные обратно в строку
ЗаписьJsonОчищенный = Новый ЗаписьJSON;
ЗаписьJsonОчищенный.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJsonОчищенный, ДанныеОбъекта);
ИтоговыйJSON = ЗаписьJsonОчищенный.Закрыть();
Важное предупреждение: При сериализации объекта типа Соответствие платформа 1С не гарантирует строго одинаковый порядок следования ключей в итоговой строке JSON при разных запусках. В отличие от Структуры, где ключи всегда упорядочиваются по алфавиту, порядок в Соответствии может зависеть от внутренней хэш-таблицы платформы. Для стабильного сравнения хэшей рекомендуется переложить очищенные данные из соответствия обратно в структуру (если в ключах больше не содержится недопустимых символов типа #).
Если вы работаете на современных версиях платформы 1С:Предприятие (начиная с релиза 8.3.23), то рутинные операции с созданием объектов ЗаписьJSON и ЧтениеJSON можно существенно упростить. Разработчикам стали доступны глобальные методы быстрого преобразования данных:
ЗаписатьЗначениеJSON() — позволяет быстро преобразовать структуру или массив в строку JSON одной строчкой кода.ПрочитатьЗначениеJSON() — выполняет обратную десериализацию строки JSON без явного объявления объектов чтения потока.Использование данных методов позволяет сократить объем кода и сделать его более читаемым:
// Пример для версии 1С 8.3.23+
ДанныеДокумента = ПолучитьДанныеДляСравнения(Выборка.Ссылка, , ИсключаемыеТЧ);
СтрокаJSON = ЗаписатьЗначениеJSON(ДанныеДокумента);
Выбор конкретного метода зависит от технических требований проекта. Если в базах гарантирована идентичность UUID всех справочников (например, базы синхронизируются через РИБ), то отлично подойдет Решение 2. Если же базы независимы и синхронизация происходит по кодам/наименованиям, то единственным верным решением будет использование Решения 1 с динамической сборкой структуры.