При организации обмена данными между 1С и внешними системами (веб-сайтами, мобильными приложениями, микросервисами) разработчики часто используют механизм XDTO для формирования XML или JSON-файлов. Однако нередко возникает ситуация: при обходе выборки номенклатуры связанные справочники (например, единицы измерения, материалы, спецификации) сериализуются полностью внутри каждого товара. Это приводит к многократному дублированию данных и лавинообразному росту размера итогового файла.
Разберем подробно, почему платформа 1С ведет себя подобным образом, с какими внутренними ограничениями объектной модели XDTO это связано и как правильно организовать выгрузку ссылочных данных.
Рассмотрим типовую ошибку при проектировании структуры пакета XSD. Если для свойства номенклатуры указан составной тип (complexType), например MeasuringUnits, то фабрика XDTO при сериализации всегда формирует вложенный узел со всеми полями, описанными в схеме:
Если ЗначениеЗаполнено(ВыборкаДанныеНоменклатуры.ЕдиницаИзмерения) Тогда
ЕдиницаИзмеренияXDTO = ФабрикаXDTO.Создать(ФабрикаXDTO.Тип(ИмяПространстваИмен, "MeasuringUnits"));
ЕдиницаИзмеренияXDTO.ID = XMLСтрока(ВыборкаДанныеНоменклатуры.ЕдиницаИзмерения.УникальныйИдентификатор());
ЕдиницаИзмеренияXDTO.Code = ВыборкаДанныеНоменклатуры.ЕдиницаИзмеренияКод;
ЕдиницаИзмеренияXDTO.Name = ВыборкаДанныеНоменклатуры.ЕдиницаИзмеренияНаименование;
ПозицияНоменклатурыXDTO.MeasuringUnit = ЕдиницаИзмеренияXDTO;
КонецЕсли;
Если в цикле по 10 000 позиций номенклатуры встречается одна и та же единица измерения «шт», фабрика создаст 10 000 одинаковых вложенных объектов. Попытка оптимизировать код путем создания объекта один раз и повторного присваивания его разным позициям номенклатуры приведет к неожиданному результату.
Проанализируем причину такого поведения: объект ОбъектXDTO представляет собой элемент единого иерархического графа (DOM-дерева). В объектной модели XDTO у каждого узла может быть строго один родитель. Если взять созданный ранее объект XDTO и присвоить его свойству другого объекта, платформа не создаст ссылку и не сделает копию — она переместит объект на новое место, удалив его из предыдущего родителя. В результате значение свойства окажется заполненным только у последнего обработанного элемента списка.
Наиболее эффективный подход к проектированию схем обмена — переход от глубоко вложенных структур к реляционной нормализации данных. Выделим справочные сущности в отдельные верхнеуровневые массивы (словари), а в номенклатуре оставим только строковые идентификаторы.
Разберем по шагам реализацию этого подхода:
ProductRange изменим тип свойства MeasuringUnit со сложного типа на примитивный xs:string (или тип GUID).ProductList) предусмотрим отдельные списки для справочников: UnitsList, MaterialsList и т.д.Соответствие для кэширования уже обработанных ссылок, чтобы не дублировать их в словаре.Посмотрим на пример кода с использованием кэширования:
ИмяПространстваИмен = "http://www.example.org/exchange";
СписокНоменклатурыXDTO = ФабрикаXDTO.Создать(ФабрикаXDTO.Тип(ИмяПространстваИмен, "ProductList"));
КэшЕдиницИзмерения = Новый Соответствие;
Пока ВыборкаДанныеНоменклатуры.Следующий() Цикл
ПозицияНоменклатурыXDTO = ФабрикаXDTO.Создать(ФабрикаXDTO.Тип(ИмяПространстваИмен, "ProductRange"));
ПозицияНоменклатурыXDTO.ID = XMLСтрока(ВыборкаДанныеНоменклатуры.Ссылка.УникальныйИдентификатор());
ПозицияНоменклатурыXDTO.Name = СокрЛП(ВыборкаДанныеНоменклатуры.Наименование);
// Обработка ссылки на единицу измерения
СсылкаЕдиница = ВыборкаДанныеНоменклатуры.ЕдиницаИзмерения;
Если ЗначениеЗаполнено(СсылкаЕдиница) Тогда
ИдентификаторЕдиницы = XMLСтрока(СсылкаЕдиница.УникальныйИдентификатор());
// Записываем в товар только строковый ID
ПозицияНоменклатурыXDTO.MeasuringUnitID = ИдентификаторЕдиницы;
// Если единицу еще не выгружали в общий список — создаем ее
Если КэшЕдиницИзмерения[СсылкаЕдиница] = Неопределено Тогда
ЕдиницаXDTO = ФабрикаXDTO.Создать(ФабрикаXDTO.Тип(ИмяПространстваИмен, "MeasuringUnitItem"));
ЕдиницаXDTO.ID = ИдентификаторЕдиницы;
ЕдиницаXDTO.Code = ВыборкаДанныеНоменклатуры.ЕдиницаИзмеренияКод;
ЕдиницаXDTO.Name = ВыборкаДанныеНоменклатуры.ЕдиницаИзмеренияНаименование;
СписокНоменклатурыXDTO.UnitsList.Добавить(ЕдиницаXDTO);
КэшЕдиницИзмерения.Вставить(СсылкаЕдиница, Истина);
КонецЕсли;
КонецЕсли;
СписокНоменклатурыXDTO.ProductItems.Добавить(ПозицияНоменклатурыXDTO);
КонецЦикла;
// Запись сформированного пакета в файл
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.ОткрытьФайл(ИмяФайлаВыгрузки);
ФабрикаXDTO.ЗаписатьJSON(ЗаписьJSON, СписокНоменклатурыXDTO);
ЗаписьJSON.Закрыть();
Если требования внешней системы не позволяют вынести справочники в корень документа, применим архитектурный шаблон, аналогичный формату EnterpriseData. В схеме XSD создадим для каждой сущности два типа данных:
MeasuringUnit) — содержит полный набор реквизитов (наименование, код, коэффициент, международное сокращение).MeasuringUnitRef) — содержит исключительно ключевые поля для идентификации объекта (как правило, только поле ID или связку Code + ID).В свойствах номенклатуры указываем тип MeasuringUnitRef. При формировании выгрузки создаем компактный объект, содержащий только уникальный идентификатор. Это позволяет строго валидировать входящие данные по схеме XSD на стороне приемника, исключая при этом раздувание объема документа.
При выгрузке больших каталогов (от десятков тысяч позиций) использование объектов XDTO накладывает существенные накладные расходы на оперативную память, так как все дерево документа строится в памяти целиком до момента вызова ЗаписатьJSON.
Рассмотрим альтернативное высокопроизводительное решение с использованием потокового объекта ЗаписьJSON и стандартных коллекций 1С (Структура и Массив):
ЗаписьJSON = Новый ЗаписьJSON;
ПараметрыЗаписиJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет);
ЗаписьJSON.ОткрытьФайл(ИмяФайлаВыгрузки, , , ПараметрыЗаписиJSON);
ЗаписьJSON.ЗаписатьНачалоОбъекта();
// 1. Секция товаров
ЗаписьJSON.ЗаписатьИмяСвойства("Products");
ЗаписьJSON.ЗаписатьНачалоМассива();
КэшЕдиниц = Новый Соответствие;
Пока ВыборкаДанныеНоменклатуры.Следующий() Цикл
ТоварСтруктура = Новый Структура;
ТоварСтруктура.Вставить("id", XMLСтрока(ВыборкаДанныеНоменклатуры.Ссылка.УникальныйИдентификатор()));
ТоварСтруктура.Вставить("name", СокрЛП(ВыборкаДанныеНоменклатуры.Наименование));
ТоварСтруктура.Вставить("code", СокрЛП(ВыборкаДанныеНоменклатуры.Код));
Если ЗначениеЗаполнено(ВыборкаДанныеНоменклатуры.ЕдиницаИзмерения) Тогда
IDЕдиницы = XMLСтрока(ВыборкаДанныеНоменклатуры.ЕдиницаИзмерения.УникальныйИдентификатор());
ТоварСтруктура.Вставить("measuringUnitId", IDЕдиницы);
Если КэшЕдиниц[IDЕдиницы] = Неопределено Тогда
СтруктураЕдиницы = Новый Структура("id,code,name",
IDЕдиницы,
ВыборкаДанныеНоменклатуры.ЕдиницаИзмеренияКод,
ВыборкаДанныеНоменклатуры.ЕдиницаИзмеренияНаименование
);
КэшЕдиниц.Вставить(IDЕдиницы, СтруктураЕдиницы);
КонецЕсли;
КонецЕсли;
ЗаписатьJSON(ЗаписьJSON, ТоварСтруктура);
КонецЦикла;
ЗаписьJSON.ЗаписатьКонецМассива();
// 2. Секция справочников
ЗаписьJSON.ЗаписатьИмяСвойства("MeasuringUnits");
ЗаписьJSON.ЗаписатьНачалоМассива();
Для Каждого КлючИЗначение Из КэшЕдиниц Цикл
ЗаписатьJSON(ЗаписьJSON, КлючИЗначение.Значение);
КонецЦикла;
ЗаписьJSON.ЗаписатьКонецМассива();
ЗаписьJSON.ЗаписатьКонецОбъекта();
ЗаписьJSON.Закрыть();
Такой подход обеспечивает минимальное потребление оперативной памяти (память не расходуется на хранение полного дерева объектов) и значительно ускоряет формирование итогового JSON-файла.