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

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

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

Анализ исходной задачи и текущего решения

Представьте, что у вас есть справочник «Номенклатура», в котором товары организованы по маркам автомобилей, а внутри каждой марки могут быть подгруппы (например, «Модели», «Запчасти») и уже в них — конкретные элементы номенклатуры. Ваша задача — получить все элементы, которые принадлежат к определенным маркам автомобилей, независимо от уровня их вложенности.

Исходный код, который вы нам предоставили, демонстрирует попытку решения этой задачи, но с одним важным ограничением:


Функция НаименованиеАвтомобилей()
    
    Массив = Новый Массив;
    Массив.Добавить("BMW");
    Массив.Добавить("CHEVROLET");
    Массив.Добавить("CHRYSLER");
    Массив.Добавить("CITROEN-PEUGEOT");
    Массив.Добавить("DAEWOO");
    Массив.Добавить("FIAT");
    Массив.Добавить("FORD");
    Массив.Добавить("HONDA");
    Возврат Массив;
    
КонецФункции

Функция ПолучитьВыборкуИзНоменклатуры() Экспорт
    
    
    Результат = Неопределено;    
    Массив = Новый Массив;
    Автомобиль = НаименованиеАвтомобилей();    
     Для Индекс = 0 По Автомобиль.Количество()-1 Цикл
        Выполнено = Ложь;
        Запрос = Новый Запрос;
        Запрос.Текст = 
        "ВЫБРАТЬ
        |    Номенклатура.Ссылка КАК Ссылка
        |ИЗ
        |    Справочник.Номенклатура КАК номенклатура
        |ГДЕ
        |    Номенклатура.Родитель.Наименование = &Автомобиль
        |    И Номенклатура.ТипНоменклатуры = &ТипНоменклатуры";
        Запрос.УстановитьПараметр("Автомобиль", Автомобиль[Индекс]);
        Запрос.УстановитьПараметр("ТипНоменклатуры", ТипНоменклатуры);
        Выборка = Запрос.Выполнить().Выбрать();
        Пока Выборка.Следующий() Цикл
            Структура = Новый Структура;
            Структура.Вставить("Марка", Автомобиль[Индекс]);
            Структура.Вставить("Ссылка", Выборка.Ссылка);
            Массив.Добавить(Структура);
            Выполнено = Истина;
                КонецЦикла;
        КонецЦикла;
    Если Массив.Количество() <> 0 Тогда
         Результат = Массив;
         Возврат Результат 
    КонецЕсли;
    
    Возврат Результат;
    
КонецФункции

Проанализируем текущий подход: в цикле для каждого наименования автомобиля (например, "BMW") формируется отдельный запрос. Условие в запросе Номенклатура.Родитель.Наименование = &Автомобиль означает, что вы ищете элементы, у которых непосредственный родитель (группа) имеет наименование, совпадающее с текущей маркой. Это приводит к тому, что будут выбраны только те элементы, которые находятся на первом уровне вложенности непосредственно под группой "BMW", "CHEVROLET" и так далее. Элементы, находящиеся глубже в иерархии (например, в подгруппе "Модели" внутри "BMW"), будут проигнорированы.

Для решения поставленной задачи нам необходимо изменить условие в запросе таким образом, чтобы оно учитывало всю иерархию под выбранной группой.

Решение 1: Использование конструкции "В ИЕРАРХИИ" для одной группы

Платформа 1С предоставляет мощную конструкцию языка запросов для работы с иерархическими данными — В ИЕРАРХИИ. Эта конструкция позволяет выбрать все элементы, которые входят в указанную группу или любую из ее подгрупп, на любой глубине вложенности. Таким образом, мы можем легко получить все необходимые элементы номенклатуры.

Однако есть важный нюанс: конструкция В ИЕРАРХИИ работает с ссылками на элементы справочника, а не с их наименованиями. Ваш текущий код использует наименования групп ("BMW", "CHEVROLET") как параметры запроса. Поэтому первым шагом нам нужно получить ссылку на каждую группу по ее наименованию.

Давайте разберем по шагам, как модифицировать ваш код для использования В ИЕРАРХИИ:

  1. Получение ссылки на группу по наименованию. Перед выполнением запроса в цикле нам нужно найти соответствующую группу в справочнике "Номенклатура" по ее наименованию. Для этого мы можем использовать метод Справочники.Номенклатура.НайтиПоНаименованию(). Этот метод позволяет найти элемент справочника по его наименованию. Если указать второй параметр Истина, поиск будет производиться по полному наименованию, что важно для уникальности.

  2. Изменение текста запроса. Мы заменим условие Номенклатура.Родитель.Наименование = &Автомобиль на Номенклатура.Ссылка В ИЕРАРХИИ(&ГруппаМарки). При этом параметр &Автомобиль мы переименуем, чтобы не было путаницы, и будем передавать в него уже не наименование, а ссылку на группу.

  3. Установка параметра запроса. Вместо наименования мы будем передавать в запрос полученную ссылку на группу.

Вот как будет выглядеть функция ПолучитьВыборкуИзНоменклатуры() после этих изменений:


Функция ПолучитьВыборкуИзНоменклатуры() Экспорт
    
    Результат = Неопределено;    
    Массив = Новый Массив;
    СписокМарок = НаименованиеАвтомобилей();    
     Для Индекс = 0 По СписокМарок.Количество()-1 Цикл
        ТекНаименованиеМарки = СписокМарок[Индекс];
        
        // Шаг 1: Получаем ссылку на группу по наименованию
        // Истина - для точного соответствия наименования
        // Справочники.Номенклатура.ПустаяСсылка() - для поиска в корне иерархии
        // Справочники.Номенклатура.ПустаяСсылка() - для поиска по владельцу (не актуально для Номенклатуры)
        ГруппаМарки = Справочники.Номенклатура.НайтиПоНаименованию(ТекНаименованиеМарки, Истина, , );
        
        // Проверяем, найдена ли группа. Если нет, пропускаем итерацию.
        Если ГруппаМарки.Пустая() Тогда
            Продолжить;
        КонецЕсли;
        
        Запрос = Новый Запрос;
        Запрос.Текст = 
        "ВЫБРАТЬ
        |    Номенклатура.Ссылка КАК Ссылка
        |ИЗ
        |    Справочник.Номенклатура КАК Номенклатура
        |ГДЕ
        |    Номенклатура.Ссылка В ИЕРАРХИИ(&ГруппаМарки)
        |    И Номенклатура.ТипНоменклатуры = &ТипНоменклатуры
        |   И Номенклатура.ЭтоГруппа = ЛОЖЬ"; // Добавим условие, чтобы не выбирать сами группы
        
        // Шаг 3: Устанавливаем параметр запроса - теперь это ССЫЛКА на группу
        Запрос.УстановитьПараметр("ГруппаМарки", ГруппаМарки);
        Запрос.УстановитьПараметр("ТипНоменклатуры", ТипНоменклатуры); // Предполагаем, что ТипНоменклатуры - это глобальная переменная или параметр.
        
        Выборка = Запрос.Выполнить().Выбрать();
        Пока Выборка.Следующий() Цикл
            Структура = Новый Структура;
            Структура.Вставить("Марка", ТекНаименованиеМарки);
            Структура.Вставить("Ссылка", Выборка.Ссылка);
            Массив.Добавить(Структура);
        КонецЦикла;
    КонецЦикла;
    
    Если Массив.Количество() <> 0 Тогда
         Результат = Массив;
         Возврат Результат; 
    КонецЕсли;
    
    Возврат Результат;
    
КонецФункции

В этом измененном коде мы сначала находим реальную ссылку на группу "BMW" (или другую марку), а затем используем эту ссылку в условии В ИЕРАРХИИ. Также мы добавили условие И Номенклатура.ЭтоГруппа = ЛОЖЬ, чтобы исключить из выборки сами группы, если вам нужны только элементы.

Этот подход решает проблему получения всех вложенных элементов. Однако, если список марок велик, вы будете выполнять множество отдельных запросов к базе данных в цикле. Каждый вызов Запрос.Выполнить() требует соединения с базой данных, обработки, передачи данных. Это может негативно сказаться на производительности.

Решение 2: Оптимизация с помощью пакетного запроса и временных таблиц

Для повышения производительности, особенно при большом количестве групп верхнего уровня, из которых необходимо извлечь иерархические данные, мы рекомендуем использовать пакетные запросы с временными таблицами. Пакетный запрос позволяет объединить несколько запросов в один "пакет", который отправляется на сервер базы данных одним запросом, минимизируя накладные расходы.

Алгоритм действий будет таким:

  1. Создание временной таблицы. На первом этапе пакетного запроса мы создадим временную таблицу (ВТ), в которую поместим ссылки на все группы верхнего уровня, по которым нам нужно получить иерархию (в нашем случае, ссылки на "BMW", "CHEVROLET" и так далее).

  2. Заполнение временной таблицы. Мы заполним эту ВТ, находя каждую группу по ее наименованию, как мы делали в первом решении.

  3. Основной запрос. На втором этапе пакетного запроса мы выполним основной запрос к справочнику "Номенклатура", используя условие Номенклатура.Ссылка В ИЕРАРХИИ(ВТ.СсылкаНаГруппу), где ВТ.СсылкаНаГруппу будет ссылаться на колонку нашей временной таблицы. Это позволит выбрать все необходимые элементы за одно обращение к базе данных.

Давайте посмотрим, как реализовать это в коде:


Функция ПолучитьВыборкуИзНоменклатурыПакетнымЗапросом() Экспорт
    
    Результат = Неопределено;    
    Массив = Новый Массив;
    СписокМарок = НаименованиеАвтомобилей();    
    
    // Подготовка списка ссылок на группы для временной таблицы
    МассивСсылокНаГруппы = Новый Массив;
    Для Индекс = 0 По СписокМарок.Количество()-1 Цикл
        ТекНаименованиеМарки = СписокМарок[Индекс];
        ГруппаМарки = Справочники.Номенклатура.НайтиПоНаименованию(ТекНаименованиеМарки, Истина, , );
        
        Если НЕ ГруппаМарки.Пустая() Тогда
            МассивСсылокНаГруппы.Добавить(ГруппаМарки);
        КонецЕсли;
    КонецЦикла;
    
    // Если группы не найдены, возвращаем пустой результат
    Если МассивСсылокНаГруппы.Количество() = 0 Тогда
        Возврат Неопределено;
    КонецЕсли;
    
    Запрос = Новый Запрос;
    // Текст пакетного запроса
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    &СписокСсылокНаГруппы КАК СсылкаНаГруппу
    |ПОМЕСТИТЬ ВТ_ГруппыМарки
    |
    |ОБЪЕДИНИТЬ ВСЕ
    |
    |ВЫБРАТЬ
    |    Номенклатура.Ссылка
    |ИЗ
    |    Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |    Номенклатура.Ссылка В ИЕРАРХИИ(ВТ_ГруппыМарки.СсылкаНаГруппу)
    |    И Номенклатура.ТипНоменклатуры = &ТипНоменклатуры
    |    И Номенклатура.ЭтоГруппа = ЛОЖЬ
    |ИТОГИ
    |   ПО Ссылка"; // Итоги нужны для группировки по ссылкам, чтобы избежать дублирования
    
    Запрос.УстановитьПараметр("СписокСсылокНаГруппы", МассивСсылокНаГруппы);
    Запрос.УстановитьПараметр("ТипНоменклатуры", ТипНоменклатуры); // Предполагаем, что ТипНоменклатуры - это глобальная переменная или параметр.
    
    // Выполняем пакетный запрос
    Выборка = Запрос.Выполнить().Выбрать();
    
    Пока Выборка.Следующий() Цикл
        Структура = Новый Структура;
        // Здесь мы не можем получить Марку напрямую из запроса без дополнительного поля.
        // Нужно будет либо изменить запрос, чтобы он включал наименование группы-родителя 
        // или наименование марки, либо определять ее после получения ссылки.
        // Для простоты примера пока пропустим заполнение поля "Марка"
        // или сделаем предположение, что Марку можно определить по родителю
        // или использовать другую логику.
        // Для изначальной задачи "Марка" была просто элементом массива НаименованиеАвтомобилей().
        // Если вам критично получить Марку из запроса, нужно усложнять запрос.
        // Например, так:
        // ВЫБРАТЬ
        //     Номенклатура.Ссылка КАК Ссылка,
        //     ВТ_ГруппыМарки.НаименованиеМарки // Если добавить это поле в ВТ
        // ИЗ
        //     Справочник.Номенклатура КАК Номенклатура
        //     ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ГруппыМарки КАК ВТ_ГруппыМарки
        //     ПО Номенклатура.Ссылка В ИЕРАРХИИ(ВТ_ГруппыМарки.СсылкаНаГруппу)
        // ...
        
        // Для текущего варианта, где Марка берется из исходного массива:
        // Придется пробегаться по исходному списку марок и определять к какой из них принадлежит текущая номенклатура.
        // Или, как в примере, просто возвращать ссылку.
        // Чтобы сохранить логику с "Марка", немного доработаем:
        // Создадим ВТ с двумя полями: СсылкаНаГруппу и НаименованиеМарки
        
        // Пересобираем запрос, чтобы учесть наименование марки
        // (Это значительное изменение, но необходимо для сохранения структуры результата)
        Запрос = Новый Запрос;
        Запрос.Текст = 
        "ВЫБРАТЬ
        |    ГруппыМарокДанные.СсылкаНаГруппу КАК СсылкаНаГруппу,
        |    ГруппыМарокДанные.НаименованиеМарки КАК НаименованиеМарки
        |ПОМЕСТИТЬ ВТ_ГруппыМаркиРасширенная
        |ИЗ
        |    &ГруппыМарокДанные КАК ГруппыМарокДанные
        |;
        |
        |ВЫБРАТЬ
        |    Номенклатура.Ссылка КАК Ссылка,
        |    ВТ_ГруппыМаркиРасширенная.НаименованиеМарки КАК Марка
        |ИЗ
        |    Справочник.Номенклатура КАК Номенклатура
        |    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ГруппыМаркиРасширенная КАК ВТ_ГруппыМаркиРасширенная
        |    ПО Номенклатура.Ссылка В ИЕРАРХИИ(ВТ_ГруппыМаркиРасширенная.СсылкаНаГруппу)
        |ГДЕ
        |    Номенклатура.ТипНоменклатуры = &ТипНоменклатуры
        |    И Номенклатура.ЭтоГруппа = ЛОЖЬ";
        
        // Создадим таблицу значений для передачи в параметр &ГруппыМарокДанные
        ТаблицаГруппДляПараметра = Новый ТаблицаЗначений;
        ТаблицаГруппДляПараметра.Колонки.Добавить("СсылкаНаГруппу", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
        ТаблицаГруппДляПараметра.Колонки.Добавить("НаименованиеМарки", Новый ОписаниеТипов("Строка"));
        
        Для Индекс = 0 По СписокМарок.Количество()-1 Цикл
            ТекНаименованиеМарки = СписокМарок[Индекс];
            ГруппаМарки = Справочники.Номенклатура.НайтиПоНаименованию(ТекНаименованиеМарки, Истина, , );
            
            Если НЕ ГруппаМарки.Пустая() Тогда
                НоваяСтрока = ТаблицаГруппДляПараметра.Добавить();
                НоваяСтрока.СсылкаНаГруппу = ГруппаМарки;
                НоваяСтрока.НаименованиеМарки = ТекНаименованиеМарки;
            КонецЕсли;
        КонецЦикла;

        Если ТаблицаГруппДляПараметра.Количество() = 0 Тогда
            Возврат Неопределено;
        КонецЕсли;
        
        Запрос.УстановитьПараметр("ГруппыМарокДанные", ТаблицаГруппДляПараметра);
        Запрос.УстановитьПараметр("ТипНоменклатуры", ТипНоменклатуры);

        Выборка = Запрос.Выполнить().Выбрать();
        Пока Выборка.Следующий() Цикл
            Структура = Новый Структура;
            Структура.Вставить("Марка", Выборка.Марка);
            Структура.Вставить("Ссылка", Выборка.Ссылка);
            Массив.Добавить(Структура);
        КонецЦикла;
    
    Если Массив.Количество() <> 0 Тогда
         Результат = Массив;
         Возврат Результат; 
    КонецЕсли;
    
    Возврат Результат;
    
КонецФункции

В этом более совершенном примере мы сначала формируем таблицу значений в коде 1С, содержащую как ссылки на группы, так и их наименования. Эту таблицу значений мы передаем в качестве параметра в пакетный запрос. Первый блок пакетного запроса создает временную таблицу ВТ_ГруппыМаркиРасширенная на основе переданной таблицы значений. Второй блок запроса уже использует эту временную таблицу для формирования условия Номенклатура.Ссылка В ИЕРАРХИИ(ВТ_ГруппыМаркиРасширенная.СсылкаНаГруппу), что позволяет нам получить все элементы номенклатуры вместе с соответствующими марками в одном запросе.

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

Дополнительные рекомендации и лучшие практики

  1. Проверка существования группы: Всегда проверяйте, что Справочники.Номенклатура.НайтиПоНаименованию() действительно нашел группу (то есть ГруппаМарки.Пустая() вернуло Ложь). Это предотвратит ошибки при попытке использовать пустую ссылку в запросе и поможет избежать нежелательных результатов.

  2. Избегайте использования пустых ссылок в В ИЕРАРХИИ: Если в параметр В ИЕРАРХИИ передать пустую ссылку, запрос может выполняться очень долго, поскольку он будет пытаться проверить принадлежность всех элементов справочника к корневому элементу, что часто интерпретируется как "все элементы справочника". Всегда передавайте конкретную ссылку на существующий элемент.

  3. Индексация: Убедитесь, что справочник "Номенклатура" и его поля, используемые в запросе (например, Родитель, ЭтоГруппа), имеют соответствующие индексы в базе данных. Это стандартная рекомендация для оптимизации любых запросов, которые работают с большими объемами данных.

  4. Условие по ТипНоменклатуры: Вы сохраняете условие Номенклатура.ТипНоменклатуры = &ТипНоменклатуры, что очень правильно, так как это дополнительно фильтрует выборку и делает ее более целевой. Это условие должно быть в вашем запросе.

  5. Условие по ЭтоГруппа: Как мы уже указывали, если вам нужны только элементы, а не группы, обязательно добавляйте условие Номенклатура.ЭтоГруппа = ЛОЖЬ. Иначе запрос В ИЕРАРХИИ выберет все группы и элементы внутри указанной иерархии.

Заключение

Мы подробно рассмотрели, как решить задачу получения всех элементов из иерархического справочника "Номенклатура", начиная с определенных групп первого уровня, включая их подгруппы. Мы начали с анализа ограничений вашего первоначального кода и предложили два основных решения:

  1. Использование конструкции В ИЕРАРХИИ в цикле для каждой группы, что является простым и понятным решением для небольшого числа групп.

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

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

← На главную