Как реализовать проверку вхождения элемента в группу «наоборот» в запросе 1С?

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

В практике разработки на платформе 1С часто возникает стандартная задача: выбрать данные, входящие в определенную группу (используя оператор В ИЕРАРХИИ). Однако иногда мы сталкиваемся с обратной ситуацией. Представьте, что в регистре сведений в качестве измерения «Контрагент» могут быть записаны как конкретные элементы, так и целые группы. У нас есть массив конкретных элементов (ссылок), и нам нужно выбрать из регистра те записи, которые относятся либо прямо к этому элементу, либо к любой из его родительских групп. В штатном языке запросов нет оператора «ЯВЛЯЕТСЯ РОДИТЕЛЕМ ДЛЯ», поэтому нам приходится использовать обходные пути. Рассмотрим подробнее, как решить эту задачу несколькими способами.

Способ 1. Программное формирование массива всех родителей

Разберем самый надежный и простой в реализации метод. Если нам нужно проверить вхождение конкретного элемента в группы, указанные в регистре, мы можем заранее подготовить список всех «предков» этого элемента. Проанализируем ситуацию: объект в 1С имеет строго ограниченную цепочку родителей (вверх до корня). Мы можем собрать их в массив и передать этот массив в запрос.

Для этого выполним следующие шаги в программном коде:

  1. Создадим пустой массив для хранения ссылок.
  2. Добавим в массив сам элемент (на случай, если в регистре указан именно он).
  3. В цикле будем подниматься по иерархии, получая родителя, пока не дойдем до пустого значения.
  4. Передадим полученный массив в параметр запроса.

Посмотрим на пример реализации такого алгоритма:


МассивРодителей = Новый Массив;
ТекущаяСсылка = ВыбранныйКонтрагент;

Пока ЗначениеЗаполнено(ТекущаяСсылка) Цикл
    МассивРодителей.Добавить(ТекущаяСсылка);
    ТекущаяСсылка = ТекущаяСсылка.Родитель;
КонецЦикла;

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Сведения.Значение
    |ИЗ
    |   РегистрСведений.НастройкиПоКонтрагентам КАК Сведения
    |ГДЕ
    |   Сведения.Контрагент В (&МассивРодителей)";

Запрос.УстановитьПараметр("МассивРодителей", МассивРодителей);

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

Способ 2. Использование разыменования полей в запросе (статическая глубина)

Если по каким-то причинам мы не можем подготовить массив заранее и нам нужно решить задачу исключительно внутри одного запроса, можно прибегнуть к соединению по родителям. Этот метод был предложен в обсуждении как быстрый, но «не универсальный» вариант. Мы ограничиваем максимальную глубину вложенности (например, до 5 или 10 уровней) и проверяем соответствие на каждом из них.

Проанализируем пример запроса, где мы проверяем вхождение товара в группу скидок:


ВЫБРАТЬ РАЗЛИЧНЫЕ
    Номенклатура.Ссылка КАК Номенклатура,
    СкрытыеСкидки.Скидка ИЗ Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СкрытыеСкидки КАК СкрытыеСкидки
    ПО (СкрытыеСкидки.ТоварГруппа = Номенклатура.Ссылка
    ИЛИ СкрытыеСкидки.ТоварГруппа = Номенклатура.Родитель
    ИЛИ СкрытыеСкидки.ТоварГруппа = Номенклатура.Родитель.Родитель
    ИЛИ СкрытыеСкидки.ТоварГруппа = Номенклатура.Родитель.Родитель.Родитель)
ГДЕ 
    Номенклатура.Ссылка В (&МассивТоваров)

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

Способ 3. Построение транзитивного замыкания (таблица связей)

Для сложных задач, когда элементов много и нужно массово сопоставить их с группами в регистре, лучше всего построить временную таблицу «Элемент — Все его предки». Это классическая задача на графы. Мы создаем временную таблицу, в которую помещаем пары (Ссылка, Ссылка), затем добавляем пары (Ссылка, Родитель), затем (Ссылка, Родитель.Родитель) и так далее.

Рассмотрим логику построения такой таблицы:

  1. Выбираем все нужные нам элементы во временную таблицу ВТ_Элементы.
  2. С помощью серии запросов ОБЪЕДИНИТЬ ВСЕ формируем таблицу ВТ_Иерархия, где для каждого элемента из ВТ_Элементы указаны его родители на всех уровнях.
  3. Соединяем ВТ_Иерархия с регистром сведений по полю «Родитель».

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

Важное предостережение: почему нельзя использовать сравнение ссылок («больше» или «меньше»)

В процессе обсуждения темы выдвигалась гипотеза, что иерархию можно проверить простым сравнением: ГДЕ СсылкаРегистра <= &Элемент. Внимание: это утверждение ошибочно и опасно!

Разберем, почему это не работает:

Современное решение: Рекурсивные временные таблицы (CTE)

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

Используя конструкцию ВЫБРАТЬ ... ИЗ ... КАК ... ВМЕСТЕ ..., мы можем построить цепочку родителей динамически. Это избавляет от необходимости писать громоздкие конструкции с ИЛИ и позволяет базе данных максимально оптимизировать выполнение задачи.

Подведем итог: для разовых задач в небольших конфигурациях оптимально использовать Способ 1 (программный сбор родителей). Для высоконагруженных систем и массовой обработки данных следует использовать рекурсивные запросы или предварительное построение таблицы связей в памяти СУБД.

← На главную