Как оптимизировать загрузку иерархического справочника из XML и избежать повторного чтения файла в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При организации обмена данными через XML разработчики часто сталкиваются с задачей переноса иерархических справочников. В исходном примере автор выгружает контрагентов в XML-файл, а при чтении выполняет обход файла дважды: первый раз для создания самих элементов и групп, а второй — для расстановки реквизита Родитель, чтобы подчиненные элементы могли найти уже созданные группы. Проанализируем, почему такой подход неэффективен, какие ошибки присутствуют в исходном коде и как построить правильную загрузку без повторного чтения файла с диска.

Ошибки и узкие места исходного кода

Прежде чем перейти к оптимизации алгоритма, разберем ключевые технические недочеты, допущенные в исходных процедурах:

  1. Блокировка файлов и отсутствие закрытия потоков: В процедуре выгрузки объект ЗаписьXML не закрывается вызовом метода Файл.Закрыть(). Это приводит к тому, что буферизованные данные могут не сброситься на диск полностью, а сам файл останется заблокированным операционной системой. При загрузке файл переоткрывается вторым экземпляром без предварительного закрытия первого.
  2. Клиент-серверные пути к файлам: Директива &НаСервереБезКонтекста означает выполнение кода на стороне сервера. Прямое обращение по пути "e:\1.xml" указывает на локальный диск серверного компьютера, а не рабочей станции пользователя. В клиент-серверной архитектуре передавать файлы между клиентом и сервером необходимо через временное хранилище или передавать строку XML.
  3. Построчное чтение и пустые узлы: Конструкция с ручным вызовом Файл.Прочитать() для получения текста наименования дает сбой, если наименование пустое и тег сформирован как самозакрывающийся. В этом случае чтение сместится к следующему узлу, что вызовет ошибку структуры.
  4. Запись объектов вне транзакции: Вызов метода Записать() в цикле для каждого объекта без использования транзакции многократно обращается к СУБД, создавая на каждую запись отдельную транзакцию, что существенно замедляет процесс.

Способ 1. Использование встроенной сериализации XDTO

Рассмотрим наиболее лаконичный и быстрый способ обмена данными между конфигурациями 1С — применение объекта СериализаторXDTO. Платформа умеет автоматически преобразовывать коллекции и объекты метаданных в XML и обратно без необходимости вручную формировать теги и атрибуты.

Посмотрим на пример выгрузки таблицы результатов запроса:


&НаСервереБезКонтекста
Процедура ВыгрузитьЧерезXDTO(ПутьКФайлу)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    Контрагенты.Код КАК Код,
        |    Контрагенты.Наименование КАК Наименование,
        |    Контрагенты.ЭтоГруппа КАК ЭтоГруппа,
        |    Контрагенты.Родитель.Код КАК КодРодителя
        |ИЗ
        |    Справочник.Контрагенты КАК Контрагенты";
    
    ТаблицаДанных = Запрос.Выполнить().Выгрузить();
    
    ЗаписьXML = Новый ЗаписьXML;
    ЗаписьXML.ОткрытьФайл(ПутьКФайлу);
    
    СериализаторXDTO.ЗаписатьXML(ЗаписьXML, ТаблицаДанных);
    
    ЗаписьXML.Закрыть();
    
КонецПроцедуры

Теперь разберем, как прочитать эти данные обратно за один вызов:


&НаСервереБезКонтекста
Процедура ЗагрузитьЧерезXDTO(ПутьКФайлу)
    
    ЧтениеXML = Новый ЧтениеXML;
    ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
    
    ТаблицаДанных = СериализаторXDTO.ПрочитатьXML(ЧтениеXML);
    ЧтениеXML.Закрыть();
    
    // Данные уже находятся в оперативной памяти в виде ТаблицыЗначений
    // Обрабатываем иерархию без повторного обращения к диску
    СоздатьЭлементыПоТаблице(ТаблицаДанных);
    
КонецПроцедуры

Кроме сериализации таблицы значений, СериализаторXDTO позволяет выгружать сами объекты справочников целиком (СправочникОбъект), сохраняя все реквизиты, табличные части и ссылки.

Способ 2. Однократное чтение файла с буферизацией иерархии в памяти

Если формат XML-файла строго задан внешней системой и его нельзя изменить, перепишем алгоритм загрузки через потоковое ЧтениеXML так, чтобы файл читался строго один раз. Для этого при первом проходе создадим элементы и запомним пары связей «Код элемента — Код родителя» в коллекции Соответствие, после чего закроем файл и проставим родителей из памяти.

Разберем реализацию этого алгоритма с оптимизацией транзакций:


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

Способ 3. Выгрузка в порядке иерархии (создание за один проход без постобработки)

Выясним, как добиться полной загрузки данных за один проход без необходимости сохранять связи в памяти. Для этого группы должны выгружаться в XML-файл раньше, чем входящие в них элементы. В таком случае к моменту создания дочернего элемента его родитель уже гарантированно записан в базу.

Для реализации этого метода изменим процедуру выгрузки:

  1. В тексте запроса строим выборку с конструкцией ИТОГИ ПО ИЕРАРХИИ.
  2. Выполняем обход выборки методом РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией).
  3. При выгрузке каждого элемента сохраняем код его родителя.

При загрузке такого файла родитель всегда будет находиться методом Справочники.Контрагенты.НайтиПоКоду(КодРодителя) непосредственно в основном цикле, так как вышестоящая папка была создана на предыдущих итерациях. Это полностью исключает второй проход и снижает расход оперативной памяти.

Особенности применения ДокументDOM

Участники обсуждения упомянули использование ДокументDOM. Этот механизм загружает XML-документ целиком в память и строит дерево объектов. С ним удобно работать при небольшом размере файла: можно перемещаться по узлам с помощью XPath или обращаться к дочерним и родительским тегам напрямую.

Однако помним об ограничении: объектная модель DOM полностью помещает структуру в оперативную память. Если в справочнике десятки или сотни тысяч записей, создание DOM-дерева приведет к переполнению памяти рабочего процесса 1С. Для объемных выгрузок всегда следует отдавать предпочтение потоковому объекту ЧтениеXML в связке с пакетными транзакциями и режимом ОбменДанными.Загрузка = Истина.

← На главную