При работе с платформой 1С:Предприятие 8.3, особенно при создании отчетов или динамических списков, мы часто сталкиваемся с необходимостью отображения иерархической структуры справочников. Типовая задача, с которой столкнулся наш пользователь, заключается в том, чтобы вывести в одном поле динамического списка полную цепочку родителей для элемента справочника, например, "Страна -> Область -> Город -> Район", когда в основной таблице документа хранится ссылка только на самый нижний элемент иерархии (в нашем примере — "Район"). Проводя быстрый анализ данных, можно заметить, что это одна из наиболее востребованных задач при проектировании интерфейсов.
Эта задача может показаться простой на первый взгляд, но имеет свои подводные камни, поскольку стандартные средства запросов 1С не предоставляют прямой поддержки рекурсивных запросов для произвольной глубины иерархии. Мы вместе разберем, почему это так, и рассмотрим несколько эффективных подходов к решению этой проблемы, чтобы вы могли выбрать наиболее подходящий для вашего случая.
Прежде чем перейти к конкретным решениям, давайте выясним, почему эта задача не решается в запросах 1С одной простой функцией. Основная причина кроется в архитектуре языка запросов 1С: он не поддерживает прямые рекурсивные запросы (так называемые CTE - Common Table Expressions, или "общие табличные выражения"), которые есть в стандартном SQL и позволяют легко обходить иерархии неизвестной глубины. В результате, когда мы хотим получить полную цепочку родителей для элемента, нам приходится прибегать к обходным путям, используя, например, интерактивное формирование текста запроса для получения родителей всех уровней.
Стандартная функция ПРЕДСТАВЛЕНИЕ(), хотя и полезная, выдает лишь наименование текущего элемента или его стандартное представление, но не полную иерархическую строку. Это означает, что для получения полного пути "от корня до текущего элемента" нам нужно либо вручную "подниматься" по иерархии, либо предзаполнять эти данные. Таким образом, мы будем вынуждены либо делать запросы, учитывающие конкретную глубину иерархии, либо выносить логику обхода в программный код.
Первый подход, который мы рассмотрим, основан на многократном использовании ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN) таблицы справочника на себя. Этот метод хорошо подходит, когда глубина иерархии фиксирована или имеет небольшое известное максимальное значение (например, не более 4-5 уровней). Мы последовательно "подтягиваем" родительские элементы, присоединяя каждую следующую таблицу справочника по полю Родитель.
Преимущества этого метода:
Недостатки:
Давайте посмотрим на пример запроса, который позволяет собрать иерархию до четырех уровней (например, "Страна -> Область -> Город -> Район"). Предположим, у нас есть табличная часть документа, которая хранит ссылку на элемент справочника Регионы, являющийся самым нижним уровнем (например, "Район").
ВЫБРАТЬ
ДокументИсточник.Регион КАК РайонСсылка,
РегионыУровень1.Наименование КАК Страна,
РегионыУровень2.Наименование КАК Область,
РегионыУровень3.Наименование КАК Город,
РегионыУровень4.Наименование КАК РайонНаименование
ИЗ
Документ.МойДокумент.ТабличнаяЧастьКАКИсточник КАК ДокументИсточник
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень4
ПО ДокументИсточник.Регион = РегионыУровень4.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень3
ПО РегионыУровень4.Родитель = РегионыУровень3.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень2
ПО РегионыУровень3.Родитель = РегионыУровень2.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень1
ПО РегионыУровень2.Родитель = РегионыУровень1.Ссылка
В этом запросе мы начинаем с самого нижнего уровня иерархии, который указан в нашей табличной части (ДокументИсточник.Регион). Затем мы последовательно присоединяем родительские элементы, используя поле Родитель справочника Регионы. Обратите внимание, что мы используем ЛЕВОЕ СОЕДИНЕНИЕ, чтобы наш запрос работал корректно, даже если какой-либо из родительских элементов отсутствует. Каждый алиас таблицы представляет собой отдельную "копию" справочника.
После того как мы получили наименования всех уровней иерархии, нам остается только собрать их в единую строку. Это можно сделать прямо в запросе, используя оператор конкатенации строк + и оператор ВЫБОР КОГДА для обработки возможных NULL значений.
ВЫБРАТЬ
ДокументИсточник.Регион,
ВЫБОР
КОГДА РегионыУровень1.Наименование ЕСТЬ NULL И РегионыУровень2.Наименование ЕСТЬ NULL И РегионыУровень3.Наименование ЕСТЬ NULL
ТОГДА РегионыУровень4.Наименование // Только элемент 4 уровня (сам район)
КОГДА РегионыУровень2.Наименование ЕСТЬ NULL И РегионыУровень3.Наименование ЕСТЬ NULL
ТОГДА РегионыУровень1.Наименование + " -> " + РегионыУровень4.Наименование // Элементы 1 и 4 уровня
КОГДА РегионыУровень3.Наименование ЕСТЬ NULL
ТОГДА РегионыУровень1.Наименование + " -> " + РегионыУровень2.Наименование + " -> " + РегионыУровень4.Наименование // Элементы 1, 2 и 4 уровня
ИНАЧЕ
РегионыУровень1.Наименование + " -> " + РегионыУровень2.Наименование + " -> " + РегионыУровень3.Наименование + " -> " + РегионыУровень4.Наименование // Все 4 уровня
КОНЕЦ КАК ПолнаяИерархия
ИЗ
Документ.МойДокумент.ТабличнаяЧастьКАКИсточник КАК ДокументИсточник
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень4
ПО ДокументИсточник.Регион = РегионыУровень4.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень3
ПО РегионыУровень4.Родитель = РегионыУровень3.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень2
ПО РегионыУровень3.Родитель = РегионыУровень2.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Регионы КАК РегионыУровень1
ПО РегионыУровень2.Родитель = РегионыУровень1.Ссылка
Второе решение является более элегантным и производительным для динамических списков. Суть подхода заключается в том, чтобы заранее, при записи элемента, формировать и сохранять полную иерархическую строку. Чтобы убедиться, что такие изменения не нарушат работу системы, можно провести анализ конфигурации на наличие ошибок, связанных с вызовами в модулях.
Преимущества этого метода:
Недостатки:
Добавьте реквизит ПолнаяИерархияСтрокой (Тип: Строка, длина неограничена). Установите индексирование, если планируете по нему искать или сортировать.
ПриЗаписи()Код размещаем в модуле объекта.
Процедура ПриЗаписи(Отказ)
Если ЭтоНовый() ИЛИ Модифицированность() Тогда
ПолнаяИерархияСтрокой = "";
ТекущийРодительСсылка = ЭтотОбъект.Родитель;
СписокНаименованийИерархии = Новый СписокЗначений;
Пока ЗначениеЗаполнено(ТекущийРодительСсылка) Цикл
ОбъектРодитель = ТекущийРодительСсылка.ПолучитьОбъект();
Если ОбъектРодитель = Неопределено Тогда
Прервать;
КонецЕсли;
СписокНаименованийИерархии.Добавить(ОбъектРодитель.Наименование);
ТекущийРодительСсылка = ОбъектРодитель.Родитель;
КонецЦикла;
Для Индекс = СписокНаименованийИерархии.Количество() - 1 По 0 Шаг -1 Цикл
Если ЗначениеЗаполнено(ПолнаяИерархияСтрокой) Тогда
ПолнаяИерархияСтрокой = ПолнаяИерархияСтрокой + " -> ";
КонецЕсли;
ПолнаяИерархияСтрокой = ПолнаяИерархияСтрокой + СписокНаименованийИерархии.Получить(Индекс).Значение;
КонецЦикла;
Если ЗначениеЗаполнено(ПолнаяИерархияСтрокой) Тогда
ЭтотОбъект.ПолнаяИерархияСтрокой = ПолнаяИерархияСтрокой + " -> " + ЭтотОбъект.Наименование;
Иначе
ЭтотОбъект.ПолнаяИерархияСтрокой = ЭтотОбъект.Наименование;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Для заполнения реквизита у созданных элементов напишите простую обработку.
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Регионы";
Результат = Запрос.Выполнить().Выбрать();
Пока Результат.Следующий() Цикл
Попытка
ОбъектРегиона = Результат.Ссылка.ПолучитьОбъект();
Если ОбъектРегиона <> Неопределено Тогда
ОбъектРегиона.Записать();
КонецЕсли;
Исключение
Сообщить("Ошибка при записи: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Запрос в динамическом списке становится тривиальным:
ВЫБРАТЬ
ДокументИсточник.Ссылка КАК СсылкаДокумента,
ДокументИсточник.Регион.ПолнаяИерархияСтрокой КАК ПолнаяИерархияРегиона
ИЗ
Документ.МойДокумент.ТабличнаяЧастьКАКИсточник КАК ДокументИсточник
Выбор метода зависит от ваших ограничений. Если вы не можете менять структуру метаданных, используйте сложные запросы. Однако для достижения максимальной отзывчивости интерфейса и удобства разработки, особенно в крупных системах, рекомендуется подход с предварительным формированием строки иерархии в реквизите. Надеемся, эти советы помогут вам в оптимизации ваших решений на базе 1С!