При проектировании и разработке конфигураций на платформе 1С:Предприятие программисты часто сталкиваются со справочниками, имеющими иерархию элементов (например, справочники «Подразделения», «Статьи затрат» или «Номенклатура»). В отличие от классической иерархии «групп и элементов», где элементами-контейнерами выступают исключительно папки, в иерархии элементов любой элемент может стать родителем для другого элемента. При этом глубина вложенности дерева может составлять 3, 4 и более уровней.
В этой статье мы подробно разберем, как правильно выбирать данные из таких справочников, проанализируем особенности работы платформы с иерархическими запросами на уровне СУБД, рассмотрим обход дерева «вниз» и «вверх», а также изучим альтернативные, более производительные методы решения этой задачи с помощью Системы компоновки данных (СКД).
Когда глубина вложенности справочника строго ограничена двумя уровнями, разработчики часто используют простое условие сравнения с родителем: ГДЕ Родитель = &Родитель. Однако, если структура организации или классификатор усложняется до 3–4 уровней вложенности, такое жесткое условие перестает работать. Оно вернет только непосредственных «детей» выбранного элемента, проигнорировав «внуков» и более глубоких потомков.
Для решения этой проблемы в платформе 1С предусмотрен специальный оператор языка запросов, а также механизмы построения итогов. Выясним, как использовать их максимально эффективно.
Если перед нами стоит задача получить сам выбранный элемент и абсолютно всех его подчиненных на любую глубину вложенности, наиболее простым и лаконичным решением является использование оператора В ИЕРАРХИИ в секции ГДЕ запроса. Рассмотрим пример реализации такого запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ
| Подразделения.Ссылка КАК Ссылка
|ИЗ
| Справочник.Подразделения КАК Подразделения
|ГДЕ
| Подразделения.Ссылка В ИЕРАРХИИ(&Ссылка)
| И НЕ Подразделения.ПометкаУдаления";
Запрос.УстановитьПараметр("Ссылка", ОсновноеПодразделение);
РезультатЗапроса = Запрос.Выполнить();
Проанализируем работу этого кода. Конструкция Ссылка В ИЕРАРХИИ(&Ссылка) автоматически определяет переданный элемент (в данном случае ОсновноеПодразделение) как корень поиска и рекурсивно выбирает все дочерние элементы, находящиеся ниже по дереву, независимо от того, сколько уровней вложенности они имеют (2, 3, 4 или больше). Фильтрация по пометке удаления НЕ ПометкаУдаления гарантирует, что мы исключим из выборки неактуальные данные.
Иногда возникает зеркальная задача: для конкретного подразделения необходимо получить всю цепочку его вышестоящих руководителей или родительских департаментов. К сожалению, оператор В ИЕРАРХИИ работает только в одном направлении — сверху вниз.
Согласно методическим рекомендациям фирмы 1С, наиболее эффективным способом решения этой проблемы без чрезмерной нагрузки на СУБД является запрос в цикле порциями. Мы выбираем родителей через точку (например, Родитель, Родитель.Родитель и так далее) порциями по несколько уровней за один проход. Разберем этот алгоритм на практике:
СписокРодителей = Новый Массив;
ТекущаяСсылка = ЦелевоеПодразделение;
Пока ЗначениеЗаполнено(ТекущаяСсылка) Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Подразделения.Родитель КАК Родитель1,
| Подразделения.Родитель.Родитель КАК Родитель2,
| Подразделения.Родитель.Родитель.Родитель КАК Родитель3
|ИЗ
| Справочник.Подразделения КАК Подразделения
|ГДЕ
| Подразделения.Ссылка = &ТекущаяСсылка";
Запрос.УстановитьПараметр("ТекущаяСсылка", ТекущаяСсылка);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Если ЗначениеЗаполнено(Выборка.Родитель1) Тогда
СписокРодителей.Добавить(Выборка.Родитель1);
КонецЕсли;
Если ЗначениеЗаполнено(Выборка.Родитель2) Тогда
СписокРодителей.Добавить(Выборка.Родитель2);
КонецЕсли;
Если ЗначениеЗаполнено(Выборка.Родитель3) Тогда
СписокРодителей.Добавить(Выборка.Родитель3);
// Перемещаем указатель на самый верхний найденный уровень для следующей итерации
ТекущаяСсылка = Выборка.Родитель3;
Иначе
// Вышестоящих уровней больше нет, прерываем цикл
Прервать;
КонецЕсли;
Иначе
Прервать;
КонецЕсли;
КонецЦикла;
Этот подход позволяет за минимальное количество обращений к базе данных (обычно за 1–2 итерации цикла) полностью воссоздать всю иерархическую ветку вверх для любого элемента.
В справочниках с иерархией элементов родительский элемент сам по себе является полноценной сущностью. По нему могут проводиться документы, начисляться зарплата или списываться затраты. При формировании отчетов с группировкой по иерархии возникает серьезная опасность продублировать числовые показатели родителя и его подчиненных.
Чтобы избежать этой проблемы, при построении итогов в запросе следует использовать ключевое слово ТОЛЬКО ИЕРАРХИЯ. Посмотрим на пример запроса, рассчитывающего обороты по подразделениям:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Подразделение КАК Подразделение,
| Продажи.СуммаОборот КАК СуммаОборот
|ИЗ
| РегистрНакопления.Продажи.Обороты КАК Продажи
|ИТОГИ
| СУММА(СуммаОборот)
|ПО
| Подразделение ИЕРАРХИЯ";
Использование конструкции ПО Подразделение ИЕРАРХИЯ заставляет платформу автоматически сгруппировать данные и рассчитать промежуточные итоги для каждого узла дерева. При обходе выборки такого запроса методом Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам) разработчик получает строгую древовидную структуру без дублирования оборотов родительских элементов.
Важно понимать, как платформа 1С транслирует оператор В ИЕРАРХИИ в инструкции для СУБД (MS SQL, PostgreSQL). Платформа генерирует рекурсивные SQL-запросы с использованием CTE (Common Table Expressions — обобщенных табличных выражений) либо создает временные таблицы, куда в цикле помещает дочерние элементы до тех пор, пока на очередном шаге не перестанут добавляться новые строки.
Критически важный нюанс: Если в качестве параметра условия В ИЕРАРХИИ(&СписокПараметров) вы передаете не одну ссылку, а массив или список значений, содержащий сотни элементов, производительность СУБД может резко упасть. СУБД будет вынуждена строить тяжелые рекурсивные деревья для каждого элемента из списка.
Для оптимизации таких тяжелых запросов рекомендуется сначала собрать плоский список всех дочерних элементов во временную таблицу в один проход, а затем выполнять левое соединение или обычный отбор В по этой временной таблице.
Если ваша конечная цель — вывод иерархического списка или отчета пользователю на форму, использовать тяжелые запросы с конструкцией В ИЕРАРХИИ крайне не рекомендуется стандартами разработки 1С. Гораздо эффективнее и быстрее работает встроенный механизм связей наборов данных в СКД.
Выясним, как настроить такую связь по шагам:
ВЫБРАТЬ
Подразделения.Ссылка КАК Ссылка,
Подразделения.Родитель КАК Родитель,
Подразделения.Наименование КАК Наименование
ИЗ
Справочник.Подразделения КАК Подразделения
Ссылка, а выражение приемника: Родитель.Благодаря этому подходу рекурсивный обход дерева выполняется силами сервера приложений 1С, а не СУБД, что существенно экономит ресурсы сервера базы данных и обеспечивает мгновенный вывод даже очень ветвистых структур (включая 10 и более уровней вложенности) без зависаний системы.