При организации обмена данными через XML разработчики часто сталкиваются с задачей переноса иерархических справочников. В исходном примере автор выгружает контрагентов в XML-файл, а при чтении выполняет обход файла дважды: первый раз для создания самих элементов и групп, а второй — для расстановки реквизита Родитель, чтобы подчиненные элементы могли найти уже созданные группы. Проанализируем, почему такой подход неэффективен, какие ошибки присутствуют в исходном коде и как построить правильную загрузку без повторного чтения файла с диска.
Прежде чем перейти к оптимизации алгоритма, разберем ключевые технические недочеты, допущенные в исходных процедурах:
ЗаписьXML не закрывается вызовом метода Файл.Закрыть(). Это приводит к тому, что буферизованные данные могут не сброситься на диск полностью, а сам файл останется заблокированным операционной системой. При загрузке файл переоткрывается вторым экземпляром без предварительного закрытия первого.&НаСервереБезКонтекста означает выполнение кода на стороне сервера. Прямое обращение по пути "e:\1.xml" указывает на локальный диск серверного компьютера, а не рабочей станции пользователя. В клиент-серверной архитектуре передавать файлы между клиентом и сервером необходимо через временное хранилище или передавать строку XML.Файл.Прочитать() для получения текста наименования дает сбой, если наименование пустое и тег сформирован как самозакрывающийся. В этом случае чтение сместится к следующему узлу, что вызовет ошибку структуры.Записать() в цикле для каждого объекта без использования транзакции многократно обращается к СУБД, создавая на каждую запись отдельную транзакцию, что существенно замедляет процесс.Рассмотрим наиболее лаконичный и быстрый способ обмена данными между конфигурациями 1С — применение объекта СериализаторXDTO. Платформа умеет автоматически преобразовывать коллекции и объекты метаданных в XML и обратно без необходимости вручную формировать теги и атрибуты.
Посмотрим на пример выгрузки таблицы результатов запроса:
&НаСервереБезКонтекста
Процедура ВыгрузитьЧерезXDTO(ПутьКФайлу)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Код КАК Код,
| Контрагенты.Наименование КАК Наименование,
| Контрагенты.ЭтоГруппа КАК ЭтоГруппа,
| Контрагенты.Родитель.Код КАК КодРодителя
|ИЗ
| Справочник.Контрагенты КАК Контрагенты";
ТаблицаДанных = Запрос.Выполнить().Выгрузить();
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(ПутьКФайлу);
СериализаторXDTO.ЗаписатьXML(ЗаписьXML, ТаблицаДанных);
ЗаписьXML.Закрыть();
КонецПроцедуры
Теперь разберем, как прочитать эти данные обратно за один вызов:
&НаСервереБезКонтекста
Процедура ЗагрузитьЧерезXDTO(ПутьКФайлу)
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
ТаблицаДанных = СериализаторXDTO.ПрочитатьXML(ЧтениеXML);
ЧтениеXML.Закрыть();
// Данные уже находятся в оперативной памяти в виде ТаблицыЗначений
// Обрабатываем иерархию без повторного обращения к диску
СоздатьЭлементыПоТаблице(ТаблицаДанных);
КонецПроцедуры
Кроме сериализации таблицы значений, СериализаторXDTO позволяет выгружать сами объекты справочников целиком (СправочникОбъект), сохраняя все реквизиты, табличные части и ссылки.
Если формат XML-файла строго задан внешней системой и его нельзя изменить, перепишем алгоритм загрузки через потоковое ЧтениеXML так, чтобы файл читался строго один раз. Для этого при первом проходе создадим элементы и запомним пары связей «Код элемента — Код родителя» в коллекции Соответствие, после чего закроем файл и проставим родителей из памяти.
Разберем реализацию этого алгоритма с оптимизацией транзакций:
&НаСервереБезКонтекста
Процедура ЗагрузитьКонтрагентовОдинПроход(ПутьКФайлу)
Файл = Новый ЧтениеXML;
Файл.ОткрытьФайл(ПутьКФайлу);
СвязиРодителей = Новый Соответствие;
НачатьТранзакцию();
Попытка
Пока Файл.Прочитать() Цикл
Если Файл.ТипУзла = ТипУзлаXML.НачалоЭлемента И Файл.Имя = "Контрагент" Тогда
Код = "";
ЭтоГруппа = Ложь;
РодительКод = "";
Пока Файл.ПрочитатьАтрибут() Цикл
Если Файл.Имя = "Код" Тогда
Код = Файл.Значение;
ИначеЕсли Файл.Имя = "ЭтоГруппа" Тогда
ЭтоГруппа = (Файл.Значение = "1");
ИначеЕсли Файл.Имя = "Родитель" Тогда
РодительКод = Файл.Значение;
КонецЕсли;
КонецЦикла;
// Считываем текстовое содержимое узла
Наименование = Файл.ПрочитатьСтроку();
Если ЗначениеЗаполнено(Код) Тогда
НайденнаяСсылка = Справочники.Контрагенты.НайтиПоКоду(Код);
Если Не ЗначениеЗаполнено(НайденнаяСсылка) Тогда
Если ЭтоГруппа Тогда
Объект = Справочники.Контрагенты.СоздатьГруппу();
Иначе
Объект = Справочники.Контрагенты.СоздатьЭлемент();
КонецЕсли;
Объект.Код = Код;
Объект.Наименование = Наименование;
Объект.ОбменДанными.Загрузка = Истина;
Объект.Записать();
КонецЕсли;
Если ЗначениеЗаполнено(РодительКод) Тогда
СвязиРодителей.Вставить(Код, РодительКод);
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Обязательно закрываем файл сразу после чтения
Файл.Закрыть();
// Второй этап: расстановка родителей из памяти
Для Каждого ЗаписьСвязи Из СвязиРодителей Цикл
СсылкаЭлемента = Справочники.Контрагенты.НайтиПоКоду(ЗаписьСвязи.Ключ);
СсылкаРодителя = Справочники.Контрагенты.НайтиПоКоду(ЗаписьСвязи.Значение);
Если ЗначениеЗаполнено(СсылкаЭлемента) И ЗначениеЗаполнено(СсылкаРодителя) Тогда
Объект = СсылкаЭлемента.ПолучитьОбъект();
Если Объект.Родитель <> СсылкаРодителя Тогда
Объект.Родитель = СсылкаРодителя;
Объект.ОбменДанными.Загрузка = Истина;
Объект.Записать();
КонецЕсли;
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Файл.Закрыть();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Выясним, как добиться полной загрузки данных за один проход без необходимости сохранять связи в памяти. Для этого группы должны выгружаться в XML-файл раньше, чем входящие в них элементы. В таком случае к моменту создания дочернего элемента его родитель уже гарантированно записан в базу.
Для реализации этого метода изменим процедуру выгрузки:
ИТОГИ ПО ИЕРАРХИИ.РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией).При загрузке такого файла родитель всегда будет находиться методом Справочники.Контрагенты.НайтиПоКоду(КодРодителя) непосредственно в основном цикле, так как вышестоящая папка была создана на предыдущих итерациях. Это полностью исключает второй проход и снижает расход оперативной памяти.
Участники обсуждения упомянули использование ДокументDOM. Этот механизм загружает XML-документ целиком в память и строит дерево объектов. С ним удобно работать при небольшом размере файла: можно перемещаться по узлам с помощью XPath или обращаться к дочерним и родительским тегам напрямую.
Однако помним об ограничении: объектная модель DOM полностью помещает структуру в оперативную память. Если в справочнике десятки или сотни тысяч записей, создание DOM-дерева приведет к переполнению памяти рабочего процесса 1С. Для объемных выгрузок всегда следует отдавать предпочтение потоковому объекту ЧтениеXML в связке с пакетными транзакциями и режимом ОбменДанными.Загрузка = Истина.