При выгрузке данных во внешние системы (например, на сайт или в маркетплейс посредством XML-файла) разработчики 1С часто сталкиваются с двумя классическими трудностями. Первая — это эффективная отладка сложных, вложенных коллекций данных (когда внутри массива лежат структуры, содержащие другие массивы). Вторая — это правильное проектирование связи типа «один ко многим» (например, один товар и его множественные характеристики).
В этой статье мы подробно разберем, как решить обе эти задачи. Мы выясним причину распространенной архитектурной ошибки, из-за которой характеристики товаров перезаписывают друг друга, оптимизируем запрос к базе данных и научимся детально просматривать любые сложные структуры в отладчике 1С.
Когда вы работаете на клиенте или сервере и хотите узнать, что находится внутри переменной ПолучитьМассив, стандартное табло отладчика может показаться не совсем информативным, особенно если коллекция имеет глубокую вложенность. Рассмотрим по шагам три основных способа анализа структуры данных.
Способ 1. Использование клавиши F2 (Показать в отдельном окне)
Это самый простой способ, о котором упоминали участники форума:
ПолучитьМассивДанных()).ПолучитьМассив, нажмите правой кнопкой мыши и выберите «Вычислить выражение» (или нажмите горячие клавиши Shift + F9).Способ 2. Быстрая визуализация через JSON (Лайфхак для сложных деревьев)
Когда массив содержит сотни элементов, а внутри каждого элемента лежит еще один массив структур, кликать по плюсикам в стандартном окне отладки становится неудобно. В таких случаях мы можем преобразовать всю коллекцию в текстовый формат JSON прямо «на лету» в окне вычисления выражений.
Для этого в окне «Вычислить выражение» (Shift + F9) введите следующий код:
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ПолучитьМассив);
СтрокаДляПросмотра = ЗаписьJSON.Закрыть();
После вычисления этого выражения откройте полученную переменную СтрокаДляПросмотра по кнопке F2. Вы увидите структурированный текст, который содержит всю иерархию вашего массива в легко читаемом виде. Это избавляет от необходимости тратить время на ручной обход элементов в отладчике.
---
Проанализируем исходный код автора темы. Главная проблема заключалась в следующем цикле:
Пока ВыборкаДетальныеЗаписиХарактеристикиТовара.Следующий() Цикл
НоваяЗапись.Характеристика = Строка(ВыборкаДетальныеЗаписиХарактеристикиТовара.Характеристика);
НоваяЗапись.ЗначениеХарактеристики = Строка(ВыборкаДетальныеЗаписиХарактеристикиТовара.ЗначениеХарактеристики);
КонецЦикла;
Выясним причину ошибки: объект НоваяЗапись представляет собой плоскую структуру (Новый Структура), у которой есть строго определенные одиночные ключи Характеристика и ЗначениеХарактеристики. Когда во внутреннем цикле мы перебираем характеристики товара (которых по факту может быть 5, 10 или более), каждая следующая итерация просто перезаписывает значения этих свойств. В итоге в массив МассивДанных уходит только одна, самая последняя характеристика товара.
Чтобы решить эту проблему, структура данных должна быть иерархической. Нам необходимо спроектировать структуру так, чтобы свойство Характеристики товара само являлось **массивом структур**, содержащих пары «Свойство-Значение».
---
Использование пакетных запросов без связи, как это делал автор, приводит к избыточному чтению данных и усложняет алгоритм сборки. Кроме того, в исходном коде автора была допущена грубая ошибка производительности: фильтрация по складу происходила в предложении ГДЕ после левого соединения с виртуальной таблицей остатков. Это заставляет СУБД сначала соединять всю таблицу товаров с остатками, а затем фильтровать результат, что приводит к критическому падению скорости на реальных базах.
Разберем правильный, оптимизированный вариант решения задачи. Мы перепишем запрос, перенесем параметры в виртуальную таблицу остатков, свяжем номенклатуру с ее характеристиками через ЛЕВОЕ СОЕДИНЕНИЕ и сгруппируем результат с помощью предложения ИТОГИ ПО.
Посмотрим на пример правильной реализации функции:
&НаСервере
Функция ПолучитьМассивДанных()
МассивДанных = Новый Массив;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Товар.Ссылка КАК Ссылка,
| Товар.Код КАК Код,
| Товар.Наименование КАК Наименование,
| Товар.Бренд КАК Бренд,
| Товар.ХарактеристикаXML КАК ОписаниеТовара,
| Товар.ЦенаПродажиРозетка КАК ЦенаПродажи,
| Товар.СсылкаНаСайт КАК СсылкаНаСайт,
| Товар.Родитель.Код КАК РодительКод,
| Товар.Родитель КАК РодительскаяКатегория,
| Товар.СсылкаФотоТовара1 КАК СсылкаФотоТовара1,
| Товар.СсылкаФотоТовара2 КАК СсылкаФотоТовара2,
| Товар.СсылкаФотоТовара3 КАК СсылкаФотоТовара3,
| Товар.СсылкаФотоТовара4 КАК СсылкаФотоТовара4,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК ОстатокТовара,
| Характеристики.Характеристика КАК Характеристика,
| Характеристики.ЗначениеХарактеристики КАК ЗначениеХарактеристики
|ИЗ
| Справочник.Номенклатура КАК Товар
| // Переносим условие по складу в параметры виртуальной таблицы для оптимизации производительности
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиПоСкладам.Остатки(, Склад = &Склад) КАК Остатки
| ПО Товар.Ссылка = Остатки.Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЗначениеХарактеристикТоваров КАК Характеристики
| ПО Товар.Ссылка = Характеристики.Номенклатура
|ГДЕ
| Товар.ТоварXml = ИСТИНА
|ИТОГИ ПО
| Ссылка"; // Группируем выборку по конкретному товару
Запрос.УстановитьПараметр("Склад", Справочники.Склады.НайтиПоНаименованию("Основной склад"));
РезультатЗапроса = Запрос.Выполнить();
// Обходим верхний уровень группировки (Товары)
ВыборкаТовары = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаТовары.Следующий() Цикл
// Создаем структуру для текущего товара
ТоварСтруктура = Новый Структура;
ТоварСтруктура.Вставить("Код", Строка(ВыборкаТовары.Код));
ТоварСтруктура.Вставить("Наименование", Строка(ВыборкаТовары.Наименование));
ТоварСтруктура.Вставить("Бренд", Строка(ВыборкаТовары.Бренд));
ТоварСтруктура.Вставить("ОписаниеТовара", Строка(ВыборкаТовары.ОписаниеТовара));
ТоварСтруктура.Вставить("ЦенаПродажи", Строка(ВыборкаТовары.ЦенаПродажи));
ТоварСтруктура.Вставить("СсылкаНаСайт", Строка(ВыборкаТовары.СсылкаНаСайт));
ТоварСтруктура.Вставить("РодительскаяКатегория", Строка(ВыборкаТовары.РодительскаяКатегория));
ТоварСтруктура.Вставить("РодительскаяКатегорияКод", Строка(ВыборкаТовары.РодительКод));
ТоварСтруктура.Вставить("СсылкаФотоТовара1", Строка(ВыборкаТовары.СсылкаФотоТовара1));
ТоварСтруктура.Вставить("СсылкаФотоТовара2", Строка(ВыборкаТовары.СсылкаФотоТовара2));
ТоварСтруктура.Вставить("СсылкаФотоТовара3", Строка(ВыборкаТовары.СсылкаФотоТовара3));
ТоварСтруктура.Вставить("СсылкаФотоТовара4", Строка(ВыборкаТовары.СсылкаФотоТовара4));
ТоварСтруктура.Вставить("ОстатокТовара", Строка(ВыборкаТовары.ОстатокТовара));
// Массив, в который мы поместим ВСЕ характеристики конкретного товара
МассивХарактеристик = Новый Массив;
// Получаем подчиненную выборку (детальные записи характеристик для этого товара)
ВыборкаДетали = ВыборкаТовары.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
// Если у товара есть характеристики, заполняем структуру и добавляем в массив
Если ЗначениеЗаполнено(ВыборкаДетали.Характеристика) Тогда
СтруктураХарактеристики = Новый Структура;
СтруктураХарактеристики.Вставить("Свойство", Строка(ВыборкаДетали.Характеристика));
СтруктураХарактеристики.Вставить("Значение", Строка(ВыборкаДетали.ЗначениеХарактеристики));
МассивХарактеристик.Добавить(СтруктураХарактеристики);
КонецЕсли;
КонецЦикла;
// Связываем массив характеристик со структурой товара
ТоварСтруктура.Вставить("Характеристики", МассивХарактеристик);
// Добавляем полностью собранный товар в общий массив данных
МассивДанных.Добавить(ТоварСтруктура);
КонецЦикла;
Возврат МассивДанных;
КонецФункции
Проанализируем, почему этот вариант оптимален: за счет использования предложения ИТОГИ ПО платформа сама разделяет результат запроса на два логических уровня. На первом уровне (ВыборкаТовары) мы получаем уникальный список товаров с их реквизитами и остатками. На втором уровне (ВыборкаДетали) мы получаем только те строки, которые относятся к текущему товару. Это исключает дублирование кода и минимизирует количество запросов к СУБД.
---
Теперь, когда у нас сформирован правильный иерархический массив данных, мы можем легко и красиво выгрузить его в XML. Рассмотрим, как это сделать с помощью последовательной записи тегов.
Разберем пример процедуры выгрузки:
&НаСервере
Процедура ВыгрузитьМассивВXML(МассивДанных, ПутьКФайлу)
ЗаписьXML = Новый ЗаписьXML;
// Открываем файл с явным указанием кодировки UTF-8
ЗаписьXML.ОткрытьФайл(ПутьКФайлу, "UTF-8");
ЗаписьXML.ЗаписатьОбъявлениеXML();
// Создаем корневой элемент выгрузки
ЗаписьXML.ЗаписатьНачалоЭлемента("Catalog");
Для Каждого Товар Из МассивДанных Цикл
ЗаписьXML.ЗаписатьНачалоЭлемента("Product");
// Выгружаем основные реквизиты товара
ЗаписьXML.ЗаписатьЭлемент("Code", Товар.Код);
ЗаписьXML.ЗаписатьЭлемент("Name", Товар.Наименование);
ЗаписьXML.ЗаписатьЭлемент("Brand", Товар.Бренд);
ЗаписьXML.ЗаписатьЭлемент("Price", Товар.ЦенаПродажи);
ЗаписьXML.ЗаписатьЭлемент("Stock", Товар.ОстатокТовара);
// Если у товара есть заполненные характеристики, выгружаем их списком
Если Товар.Характеристики.Количество() > 0 Тогда
ЗаписьXML.ЗаписатьНачалоЭлемента("Characteristics");
Для Каждого Хар Из Товар.Характеристики Цикл
ЗаписьXML.ЗаписатьНачалоЭлемента("Characteristic");
ЗаписьXML.ЗаписатьЭлемент("Name", Хар.Свойство);
ЗаписьXML.ЗаписатьЭлемент("Value", Хар.Значение);
ЗаписьXML.ЗаписатьКонецЭлемента(); // Characteristic
КонецЦикла;
ЗаписьXML.ЗаписатьКонецЭлемента(); // Characteristics
КонецЕсли;
ЗаписьXML.ЗаписатьКонецЭлемента(); // Product
КонецЦикла;
ЗаписьXML.ЗаписатьКонецЭлемента(); // Catalog
// Обязательно закрываем объект ЗаписьXML для физического сохранения данных на диске
ЗаписьXML.Закрыть();
КонецПроцедуры
Благодаря тому, что мы заранее подготовили правильную иерархическую коллекцию в коде 1С, процесс формирования XML стал линейным, понятным и надежным. Такой код легко поддерживать, масштабировать и при необходимости адаптировать под требования схемы XSD принимающей стороны.