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

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

При импорте данных или интеграции со сторонними системами разработчики часто сталкиваются с задачей построения иерархического справочника на основе плоской таблицы. Типичный пример — создание структуры Улица (группа) -> Дом (группа) -> Прибор учета (элемент). Основная сложность заключается в том, чтобы не создавать пустые группы (например, улицу без домов или дом без приборов) и при этом избежать лавинообразного роста запросов к базе данных при проверке дублей.

В этой статье мы подробно разберем, как решить эту задачу изящно и профессионально. Мы откажемся от простых, но неэффективных методов «ручного» перебора плоской таблицы и обратимся к мощным штатным механизмам платформы 1С: конструкциям языка запросов, обходу результатов по группировкам и механизму кэширования в оперативной памяти.

Анализ проблемы и выбор типа соединения таблиц

Рассмотрим ситуацию, описанную автором темы. Имеются три сущности: улицы, дома и приборы учета. Нам нужно построить иерархию только для тех объектов, которые реально существуют в связке. То есть, если в доме нет ни одного прибора, то создавать группу для этого дома не имеет смысла. Аналогично, если на улице нет домов с приборами, саму улицу создавать в справочнике тоже не нужно.

Для решения этой задачи в первую очередь необходимо правильно составить запрос к базе данных. На форуме предлагалось использовать ЛЕВОЕ СОЕДИНЕНИЕ. Однако при левом соединении в результат попадут улицы, для которых нет домов, и дома, для которых нет приборов (в соответствующих полях будет значение Null).

Чтобы автоматически отсечь «пустые» ветки иерархии еще на уровне СУБД, нам необходимо использовать ВНУТРЕННЕЕ СОЕДИНЕНИЕ (Inner Join). В этом случае в выборку попадут только те строки, для которых заполнены все три звена цепочки: и улица, и дом, и прибор.

Шаг 1. Использование конструкции ИТОГИ ПО для построения иерархии в запросе

Вместо того чтобы выбирать плоскую таблицу и затем программно отслеживать смену улицы или дома в цикле, мы переложим задачу группировки на СУБД. Для этого в языке запросов 1С предусмотрена мощная секция ИТОГИ ПО. Она позволяет выстроить древовидную структуру данных прямо в результате запроса.

Проанализируем пример запроса, который решает эту задачу:


ВЫБРАТЬ
    dbo_Street.Name КАК StreetName,
    dbo_Dom.Name КАК DomName,
    dbo_Pribor.Name КАК PriborName
ИЗ
    dbo_Street КАК dbo_Street
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ dbo_Dom КАК dbo_Dom 
        ПО dbo_Street.СвязующееПоле = dbo_Dom.СвязующееПоле
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ dbo_Pribor КАК dbo_Pribor 
        ПО dbo_Dom.СвязующееПоле = dbo_Pribor.СвязующееПоле
ИТОГИ ПО
    StreetName,
    DomName

Благодаря предложению ИТОГИ ПО, платформа сгруппирует результат. На верхнем уровне иерархии будут находиться уникальные названия улиц, на втором уровне — уникальные дома для каждой конкретной улицы, а на самом нижнем (детальном) уровне — конечные приборы учета.

Шаг 2. Обход результата запроса по группировкам

После выполнения запроса с итогами мы не можем обходить его результат линейным методом Выбрать(), так как это приведет к чтению дублирующихся данных. Мы применим метод обхода по группировкам, передавая в метод Выбрать() системное перечисление ОбходРезультатаЗапроса.ПоГруппировкам.

Разберем по шагам, как выглядит этот алгоритм в коде:


РезультатЗапроса = Запрос.Выполнить();

// 1. Обходим первый уровень иерархии — Улицы
ВыборкаУлицы = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаУлицы.Следующий() Цикл
    
    // Здесь мы создаем или находим родительскую группу "Улица"
    ГруппаУлица = ПолучитьИлиСоздатьГруппу(ВыборкаУлицы.StreetName, Справочники.Адреса.ПустаяСсылка());
    
    // 2. Обходим второй уровень иерархии — Дома внутри текущей Улицы
    ВыборкаДома = ВыборкаУлицы.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
    Пока ВыборкаДома.Следующий() Цикл
        
        // Создаем или находим подчиненную группу "Дом" внутри "Улицы"
        ГруппаДом = ПолучитьИлиСоздатьГруппу(ВыборкаДома.DomName, ГруппаУлица);
        
        // 3. Обходим детальные записи — Приборы внутри текущего Дома
        // Здесь мы используем обычный обход, так как это конечные элементы
        ВыборкаПриборы = ВыборкаДома.Выбрать();
        Пока ВыборкаПриборы.Следующий() Цикл
            
            // Создаем элемент "Прибор" в группе "Дом"
            СоздатьЭлементПрибор(ВыборкаПриборы.PriborName, ГруппаДом);
            
        КонецЦикла;
    КонецЦикла;
КонецЦикла;

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

Шаг 3. Оптимизация производительности с помощью кэширования

При записи большого объема данных в цикле критически важным фактором становится скорость работы алгоритма. Если для каждой итерации мы будем выполнять поиск группы в базе данных через метод НайтиПоНаименованию, это приведет к лавинообразному росту обращений к СУБД. Наш код начнет работать крайне медленно.

Чтобы избежать этой проблемы, профессиональные разработчики применяют кэширование созданных элементов в оперативной памяти с помощью объекта Соответствие. Ключом кэша будет выступать составная строка, уникально идентифицирующая группу (например, «Родитель_ИмяГруппы»), а значением — полученная или созданная СправочникСсылка.

Проанализируем функцию создания групп с использованием кэша:


// Кэш создается один раз перед началом обхода выборки
Перем КэшГрупп;
КэшГрупп = Новый Соответствие;

Функция ПолучитьИлиСоздатьГруппу(Знач ИмяГруппы, Знач РодительСсылка)
    
    ИмяГруппы = СокрЛП(ИмяГруппы);
    Если ИмяГруппы = "" Тогда
        Возврат РодительСсылка;
    КонецЕсли;
    
    // Формируем уникальный ключ для поиска в кэше
    КлючКэша = Строка(РодительСсылка.УникальныйИдентификатор()) + "_" + ВРег(ИмяГруппы);
    
    // Пробуем получить ссылку из кэша в оперативной памяти
    СсылкаНаГруппу = КэшГрупп.Получить(КлючКэша);
    
    Если СсылкаНаГруппу = Неопределено Тогда
        
        // Если в кэше нет, ищем один раз в базе данных
        СсылкаНаГруппу = Справочники.Адреса.НайтиПоНаименованию(ИмяГруппы, Истина, РодительСсылка);
        
        Если СсылкаНаГруппу.Пустая() Тогда
            // Если в базе данных группы нет, создаем её
            НоваяГруппа = Справочники.Адреса.СоздатьГруппу();
            НоваяГруппа.Наименование = ИмяГруппы;
            НоваяГруппа.Родитель = РодительСсылка;
            
            Попытка
                НоваяГруппа.Записать();
                СсылкаНаГруппу = НоваяГруппа.Ссылка;
            Исключение
                ВызватьИсключение "Не удалось записать группу справочника: " + ОписаниеОшибки();
            КонецПопытки;
        КонецЕсли;
        
        // Помещаем найденную или созданную ссылку в кэш
        КэшГрупп.Вставить(КлючКэша, СсылкаНаГруппу);
        
    КонецЕсли;
    
    Возврат СсылкаНаГруппу;
    
КонецФункции

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

Альтернативный вариант: Выгрузка в Дерево Значений

Если ваша процедура заполнения справочника спроектирована так, что на вход ей требуется готовая коллекция объектов, мы можем выгрузить результат нашего запроса с итогами в объект ДеревоЗначений. Платформа 1С умеет делать это автоматически всего одной строкой кода:


ДеревоДанных = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

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

Ускорение работы через транзакции

Для достижения максимальной скорости работы при записи сотен или тысяч элементов справочника настоятельно рекомендуется оборачивать весь цикл создания в единую транзакцию. Это сократит накладные расходы СУБД на фиксацию каждой отдельной записи на диске.


НачатьТранзакцию();
Попытка
    
    // Здесь выполняется наш цикл обхода и вызов ПолучитьИлиСоздатьГруппу
    
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ЗаписьЖурналаРегистрации("ИмпортДанных", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
    ВызватьИсключение "Произошла ошибка при импорте данных. Процесс отменен.";
КонецПопытки;

Использование транзакции в сочетании с оперативным кэшированием групп позволяет ускорить процесс создания элементов в десятки раз, обеспечивая стабильную и надежную работу системы даже на больших объемах данных.

← На главную