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

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

В этой статье мы подробно рассмотрим, как вывести иерархические группировки в системе компоновки данных (СКД) 1С, когда исходные данные представлены в плоском виде и не имеют встроенной иерархической структуры, присущей, например, справочникам.

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

Важно понимать, что язык запросов 1С, в отличие от некоторых систем управления базами данных (СУБД), не поддерживает прямое использование рекурсивных табличных выражений (Common Table Expressions - CTE) вида WITH RECURSIVE. Это означает, что мы не можем одним сложным запросом динамически построить всю иерархию неизвестной глубины. Из-за этого ограничения нам придется использовать обходные пути или адаптировать стандартные механизмы СКД.

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

Метод 1: Программное преобразование данных в плоскую таблицу для каждого уровня

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

Шаг 1: Подготовка исходных данных

Предположим, у нас есть исходные данные, которые описывают иерархическую структуру. Эти данные могут храниться в регистре сведений, таблице значений или быть результатом какого-либо другого запроса. Для ясности, представим наши данные в виде ТаблицаЗначений. Каждая запись содержит информацию о "Главе" (сам элемент), его "Родителе" и "Уровне" в иерархии. Если элемент является корневым, его "Родитель" может быть пустым или отсутствовать.


ТипСтр = Новый ОписаниеТипов("Строка",,,, Новый КвалификаторыСтроки(20));
ТипЧис = Новый ОписаниеТипов("Число",,, Новый КвалификаторыЧисла(10, 2));

Таб = Новый ТаблицаЗначений;
Таб.Колонки.Добавить("Глава", ТипСтр);
Таб.Колонки.Добавить("Родитель", ТипСтр);
Таб.Колонки.Добавить("Уровень", ТипЧис);

Нов = Таб.Добавить(); Нов.Глава = "А"; Нов.Родитель =  ""; Нов.Уровень = 1;
Нов = Таб.Добавить(); Нов.Глава = "Б"; Нов.Родитель = "А"; Нов.Уровень = 2;
Нов = Таб.Добавить(); Нов.Глава = "В"; Нов.Родитель = "Б"; Нов.Уровень = 3;
Нов = Таб.Добавить(); Нов.Глава = "Г"; Нов.Родитель =  ""; Нов.Уровень = 1;
Нов = Таб.Добавить(); Нов.Глава = "Д"; Нов.Родитель =  ""; Нов.Уровень = 1;
Нов = Таб.Добавить(); Нов.Глава = "Е"; Нов.Родитель = "Д"; Нов.Уровень = 2;
Нов = Таб.Добавить(); Нов.Глава = "И"; Нов.Родитель =  ""; Нов.Уровень = 1;

В этом примере мы видим, как элементы "Б" и "Е" подчинены "А" и "Д" соответственно, а "В" подчинен "Б". Элементы "А", "Г", "Д", "И" являются корневыми (первый уровень), так как у них нет родителя.

Шаг 2: Построение плоской таблицы с помощью запроса

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

Рассмотрим следующий запрос, который преобразует наши исходные данные:


Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Таб", Таб);
Запрос.Текст =
    "ВЫБРАТЬ
    |    Таб.Глава КАК Глава,
    |    Таб.Родитель КАК Родитель,
    |    Таб.Уровень КАК Уровень
    |ПОМЕСТИТЬ Таб
    |ИЗ
    |    &Таб КАК Таб
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    Таб.Глава КАК Глава,
    |    Таб.Родитель КАК Родитель,
    |    Таб.Уровень КАК Уровень
    |ПОМЕСТИТЬ ВТ_1
    |ИЗ
    |    Таб КАК Таб
    |ГДЕ
    |    Таб.Уровень = 1
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    Таб.Глава КАК Глава,
    |    Таб.Родитель КАК Родитель,
    |    Таб.Уровень КАК Уровень
    |ПОМЕСТИТЬ ВТ_2
    |ИЗ
    |    Таб КАК Таб
    |ГДЕ
    |    Таб.Уровень = 2
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    Таб.Глава КАК Глава,
    |    Таб.Родитель КАК Родитель,
    |    Таб.Уровень КАК Уровень
    |ПОМЕСТИТЬ ВТ_3
    |ИЗ
    |    Таб КАК Таб
    |ГДЕ
    |    Таб.Уровень = 3
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    ВТ_1.Глава КАК Глава1,
    |    ВТ_2.Глава КАК Глава2,
    |    ВТ_3.Глава КАК Глава3
    |ИЗ
    |    ВТ_1 КАК ВТ_1
    |        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_2 КАК ВТ_2
    |        ПО ВТ_1.Глава = ВТ_2.Родитель
    |            ЛЕВОЕ СОЕДИНЕНИЕ ВТ_3 КАК ВТ_3
    |            ПО ВТ_2.Глава = ВТ_3.Родитель";

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

Давайте разберем этот запрос по шагам:

  1. Сначала мы помещаем нашу исходную ТаблицаЗначений (переданную как параметр &Таб) во временную таблицу Таб в запросе. Это удобно для дальнейшей работы с данными.

  2. Затем мы создаем отдельные временные таблицы (ВТ_1, ВТ_2, ВТ_3 и т.д.) для каждого известного уровня иерархии. Для этого мы выбираем из основной таблицы Таб элементы, у которых поле Уровень соответствует номеру временной таблицы.

  3. В заключительном блоке запроса мы выполняем серию ЛЕВЫХ СОЕДИНЕНИЙ. Это ключевой момент. Мы связываем элементы первого уровня (из ВТ_1) с их прямыми подчиненными второго уровня (из ВТ_2) по условию ВТ_1.Глава = ВТ_2.Родитель. Затем результат этого соединения мы аналогично соединяем с элементами третьего уровня (из ВТ_3) по условию ВТ_2.Глава = ВТ_3.Родитель. И так далее, для каждого последующего уровня.

  4. Важно использовать ЛЕВОЕ СОЕДИНЕНИЕ, чтобы в итоговый результат попали все элементы верхних уровней, даже если у них нет подчиненных на следующих уровнях. Например, если у "Д" есть "Е" (Уровень 2), но нет подчиненных Уровня 3, то Глава3 для этой строки будет NULL.

  5. В результате получаем плоскую таблицу с колонками Глава1, Глава2, Глава3 (и так далее, сколько уровней мы соединили). Здесь Глава1 — это элемент первого уровня, Глава2 — его подчиненный второго уровня, и Глава3 — подчиненный третьего уровня.

Шаг 3: Использование плоской таблицы в СКД

Полученную плоскую таблицу (переменная Выборка) мы теперь готовы передать в схему компоновки данных. СКД легко работает с такой структурой, поскольку для каждого уровня иерархии есть своя отдельная колонка.

  1. Создание набора данных: В схеме компоновки данных добавьте новый "Набор данных - объект". Этот тип набора данных позволяет передавать в СКД данные, подготовленные программно на стороне 1С.

  2. Указание имени объекта: В свойствах этого набора данных в поле "Имя объекта" укажите имя переменной, которая содержит вашу плоскую таблицу (например, "МоиДанные"). Перед формированием отчета вам нужно будет присвоить вашей переменной Выборка это имя в обработчике события, например, ПриКомпоновкеРезультата: КомпоновщикМакета.УстановитьОбъект("МоиДанные", Выборка);

  3. Проверка полей: На закладке "Поля" набора данных убедитесь, что все созданные вами колонки (Глава1, Глава2, Глава3 и т.д.) отображаются. Если они не появились автоматически, добавьте их вручную.

  4. Настройка группировок: На закладке "Настройки" отчета создайте иерархические группировки:

    • Создайте первую группировку по полю Глава1. Это будет верхний уровень иерархии.

    • Внутри этой первой группировки создайте вложенную группировку по полю Глава2.

    • Внутри группировки по Глава2 создайте вложенную группировку по полю Глава3.

    • Продолжайте эту структуру для всех уровней, которые вы подготовили в запросе.

  5. Выбранные поля: Для каждой группировки в разделе "Выбранные поля" включите соответствующее поле (например, для группировки по Глава1 добавьте Глава1, для Глава2Глава2 и т.д.). Также можно добавить детальные записи, если они требуются.

  6. Тип группировки: Если вы хотите, чтобы СКД отображала стандартные знаки раскрытия/скрытия иерархии (+/-), установите для каждой группировки тип "Иерархия" или "Только иерархия" в свойствах группировки.

Этот подход позволяет СКД корректно отобразить иерархию, поскольку для каждого уровня есть своя колонка, и СКД просто группирует по этим колонкам последовательно, создавая необходимое древовидное представление.

Метод 2: Построение иерархии через связи наборов данных в СКД

Альтернативный способ, который часто используется для построения иерархии по полям ссылочного типа (например, справочников с иерархией групп или элементов) или для произвольных иерархических структур, это использование механизма "Связи наборов данных" в самой СКД. Этот метод позволяет СКД самостоятельно рекурсивно достраивать иерархию, но требует более тонкой настройки.

Принцип работы

Метод основан на создании нескольких наборов данных, которые описывают "родительско-дочерние" связи. Один набор данных определяет "корневые" элементы, а другие наборы (часто один и тот же, но с параметром) используются для получения подчиненных элементов. СКД затем рекурсивно вызывает "дочерние" наборы данных, передавая им значение родителя.

Рассмотрим ситуацию, аналогичную той, что была в форуме, с регистром сведений РегистрацияРеферала, который содержит поля ИнвесторРеферал (сам элемент), ИнвесторРеферер (родительский элемент, тот кто пригласил) и Уровень.

Шаг 1: Определение наборов данных

Мы создаем два набора данных в схеме компоновки данных:

  1. Набор данных 1 (Корневые элементы): Этот набор данных должен выбирать элементы, которые являются "вершинами" иерархии, то есть не имеют родителя или их родитель определен особым образом как корневой. В примере автора это условие РегистрацияРеферала.ИнвесторРеферал = РегистрацияРеферала.ИнвесторРеферер, что означает, что элемент является корневым для своей ветки. Также можно использовать условие РегистрацияРеферала.ИнвесторРеферер ЕСТЬ NULL, если корневые элементы не имеют родителя вообще.

    
    ВЫБРАТЬ
        РегистрацияРеферала.ИнвесторРеферал КАК Инвестор,
        РегистрацияРеферала.Уровень КАК Уровень
    ИЗ
        РегистрСведений.РегистрацияРеферала КАК РегистрацияРеферала
    ГДЕ
        РегистрацияРеферала.ИнвесторРеферал = РегистрацияРеферала.ИнвесторРеферер
    

    Этот запрос возвращает все элементы, которые являются верхнеуровневыми.

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

    
    ВЫБРАТЬ
        ШАА_РегистрацияРеферала.ИнвесторРеферал КАК Инвестор,
        ШАА_РегистрацияРеферала.ИнвесторРеферер КАК Реферер,
        ШАА_РегистрацияРеферала.Уровень КАК Уровень
    ИЗ
        РегистрСведений.ШАА_РегистрацияРеферала КАК ШАА_РегистрацияРеферала
    ГДЕ
        ШАА_РегистрацияРеферала.ИнвесторРеферер В (&Родитель)
    

    Здесь &Родитель — это параметр, который будет динамически заполняться значениями поля Инвестор из родительской группировки (например, из Набора данных 1) или из других уровней иерархии.

Шаг 2: Настройка связей наборов данных

Перейдите на вкладку "Связи наборов данных" в схеме компоновки данных. Здесь мы определяем, как СКД будет рекурсивно получать дочерние элементы.

  1. Добавление новой связи: Создайте новую связь.

  2. Источник: Укажите "Набор данных 1" (наш набор с корневыми элементами).

  3. Приемник: Укажите "Набор данных 2" (наш набор с подчиненными элементами).

  4. Выражение источник: Здесь мы указываем поле из набора-источника, которое является "родителем" для следующего уровня. В нашем случае это Инвестор из Набора данных 1.

  5. Выражение приемник: Здесь мы указываем поле из набора-приемника, которое является "подчиненным" и по которому будет происходить отбор. Это также Инвестор из Набора данных 2.

  6. Параметр: Укажите имя параметра, который определен в запросе Набора данных 2 (в нашем случае Родитель). СКД будет автоматически передавать значение из "Выражения источника" в этот параметр "Набора данных 2" для построения следующего уровня.

Эта связь инструктирует СКД: "Для каждого элемента из Набора данных 1 (источник), возьми его значение поля Инвестор и используй его для заполнения параметра &Родитель в Наборе данных 2 (приемник), чтобы получить его прямых подчиненных."

Шаг 3: Настройка СКД для иерархического вывода

Заключительный этап — настройка внешнего вида отчета в СКД:

  1. Создание группировки: На закладке "Настройки" отчета добавьте группировку по полю Инвестор из Набора данных 1.

  2. Ключевая настройка "Проверка иерархии": Выберите созданную группировку (по Инвестор). В окне "Свойства группировки" для поля Инвестор (на вкладке "Поля" или "Дополнительные настройки") найдите флажок "Проверка иерархии". Установите его и обязательно укажите, что достраивать иерархию нужно через "Набор данных 2" (ваш набор с подчиненными элементами).

    Именно эта настройка заставляет СКД рекурсивно вызывать "Набор данных 2", используя настроенную связь.

  3. Тип группировки: Убедитесь, что для группировки установлен тип "Иерархия" или "Только иерархия", если вы ожидаете стандартное древовидное представление с возможностью раскрытия/скрытия.

  4. Выбранные поля: В разделе "Выбранные поля" для группировки по Инвестор добавьте поле Инвестор. Вы также можете добавить другие поля из Набора данных 1 или Набора данных 2, но основная иерархия будет строиться по полю, для которого активирована "Проверка иерархии".

При правильной настройке СКД будет рекурсивно вызывать Набор данных 2 для каждого элемента Набора данных 1, передавая ему Инвестор как &Родитель, тем самым строя иерархию автоматически.

Важные замечания и возможные сложности

Мы выясним, почему иногда возникают трудности с реализацией иерархического вывода в СКД.

Ограничение глубины иерархии в Методе 1 (программное преобразование)

  1. Известная глубина: Метод программного построения плоской таблицы (Метод 1) требует, чтобы вы заранее знали максимальную глубину иерархии или были готовы динамически генерировать достаточное количество соединений в запросе (ВТ_1, ВТ_2, ..., ВТ_N). Если глубина иерархии очень большая и неизвестная, создание запроса с множеством ЛЕВЫХ СОЕДИНЕНИЙ может стать громоздким и негибким.

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

Проблемы с Методом 2 (Связи наборов данных)

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

  1. Неверные условия отбора для корневых элементов: Убедитесь, что условие отбора для корневых элементов в Наборе данных 1 (например, ГДЕ РегистрацияРеферала.ИнвесторРеферал = РегистрацияРеферала.ИнвесторРеферер или ГДЕ РегистрацияРеферала.ИнвесторРеферер ЕСТЬ NULL) корректно определяет верхний уровень иерархии. Если корневые элементы не выбраны правильно, иерархия не будет построена.

  2. Ошибка в параметре связи: Название параметра в запросе подчиненного набора данных (например, &Родитель) должно точно совпадать с именем, указанным в настройках связи СКД на вкладке "Связи наборов данных". Любая опечатка приведет к тому, что параметры не будут передаваться.

  3. Неактивированный флажок "Проверка иерархии": Это одна из самых частых причин. Активация флажка "Проверка иерархии" для поля, по которому строится группировка, и правильное указание дочернего набора данных в свойствах этой группировки — критически важный шаг, который часто упускается. Без этой настройки СКД не знает, как достраивать иерархию.

  4. Отсутствие связи между наборами данных: Если связь между наборами данных не создана или создана некорректно, СКД не сможет определить, как получить подчиненные элементы.

  5. Задвоение элементов: Иногда при использовании иерархии элементов может возникать дублирование, когда родительский элемент отображается и как группа, и как отдельная строка. Это может потребовать дополнительных настроек на вкладке "Настройки" СКД, например, использования только полей группировок без вывода детальных записей, или настройки выражений для выбранных полей, чтобы исключить дублирование.

  6. Использование набора данных "Объект" в связях: В некоторых случаях возникают сложности при попытке построить иерархию через связи, если один из наборов данных является "Набором данных - объект". Более надежным может быть использование запросов к реальным данным или временным таблицам.

Системное поле УровеньВГруппировке

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

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

← На главную