При построении отчетов, реестров товаров или прайс-листов часто возникает задача вывести полную цепочку иерархии (всех родительских групп) для каждой позиции номенклатуры в одну ячейку. Например, мы хотим получить строку вида: "Для дома / Мебель / Кухонная мебель" для элемента "Стул".
В этой статье мы подробно разберем, как решить эту задачу различными методами: от простого запроса с жестким ограничением вложенности до программных алгоритмов и встроенных механизмов Системы компоновки данных (СКД). Мы проанализируем частые ошибки разработчиков и выберем оптимальные подходы.
---
Этот способ идеально подходит, если глубина иерархии в вашей базе данных ограничена и заранее известна (например, не превышает 4-5 уровней). Мы можем написать один запрос, который на уровне СУБД склеит наименования родительских групп.
Однако здесь часто совершают критическую ошибку. Рассмотрим типичный нерабочий пример, который пытаются использовать начинающие разработчики:
// ОШИБОЧНЫЙ КОД! Сравнение ссылки со строкой
ВЫБОР
КОГДА СпрНоменклатура.Родитель = ""
ТОГДА СпрНоменклатура.Родитель.Родитель
...
Выясним причину ошибки: в базе данных 1С ссылочные поля (такие как Родитель или Ссылка) хранятся в виде уникальных идентификаторов (UUID). Попытка сравнить ссылку с пустой строкой "" на уровне базы данных всегда будет возвращать Ложь (или вызовет ошибку несовместимости типов). Из-за этого запрос всегда будет уходить в ветку ИНАЧЕ, возвращая некорректный или пустой результат.
Чтобы исправить это, мы должны использовать специальную конструкцию ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка). Ниже представлен правильный и оптимизированный запрос, который выводит цепочку родителей (до 4 уровней) от верхнего к нижнему:
ВЫБРАТЬ
СпрНоменклатура.Ссылка КАК Товар,
ВЫБОР
// Если у номенклатуры вообще нет родителя (лежит в корне)
КОГДА СпрНоменклатура.Родитель = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
ТОГДА ""
// Если есть только 1 уровень родителя
КОГДА СпрНоменклатура.Родитель.Родитель = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
ТОГДА СпрНоменклатура.Родитель.Наименование
// Если есть 2 уровня родителей
КОГДА СпрНоменклатура.Родитель.Родитель.Родитель = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
ТОГДА СпрНоменклатура.Родитель.Родитель.Наименование + " / " + СпрНоменклатура.Родитель.Наименование
// Если есть 3 уровня родителей
КОГДА СпрНоменклатура.Родитель.Родитель.Родитель.Родитель = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
ТОГДА СпрНоменклатура.Родитель.Родитель.Родитель.Наименование + " / " + СпрНоменклатура.Родитель.Родитель.Наименование + " / " + СпрНоменклатура.Родитель.Наименование
// Если уровней 4 и более
ИНАЧЕ СпрНоменклатура.Родитель.Родитель.Родитель.Родитель.Наименование + " / " + СпрНоменклатура.Родитель.Родитель.Родитель.Наименование + " / " + СпрНоменклатура.Родитель.Родитель.Наименование + " / " + СпрНоменклатура.Родитель.Наименование
КОНЕЦ КАК ПутьКНоменклатуре
ИЗ
Справочник.Номенклатура КАК СпрНоменклатура
ГДЕ
НЕ СпрНоменклатура.ЭтоГруппа
Преимущества метода: выполняется чрезвычайно быстро на стороне СУБД за один проход, не требует написания серверного кода на встроенном языке.
Недостатки: если пользователь создаст группу 6-го или 7-го уровня вложенности, данные о самых верхних родителях начнут "обрезаться" или отображаться некорректно.
---
Если глубина дерева папок неизвестна или может быть очень большой, использовать конструкции ВЫБОР КОГДА в запросе становится нецелесообразно. В этом случае мы должны обрабатывать результат программно.
Частая ошибка программистов при решении этой задачи "в лоб" — обращение через точку в цикле:
// ТАК ДЕЛАТЬ НЕ РЕКОМЕНДУЕТСЯ! (Проблема N+1 запросов)
Пока ЗначениеЗаполнено(ТекущийРодитель) Цикл
СтрокаПути = СтрокаПути + " / " + ТекущийРодитель.Наименование;
ТекущийРодитель = ТекущийРодитель.Родитель; // Каждый проход цикла делает отдельный запрос к базе!
КонецЦикла;
Для предотвращения перегрузки СУБД фирма "1С" рекомендует запрашивать данные пакетами по несколько уровней иерархии за один раз. Разберем эффективный алгоритм, который получает до 5 уровней родителей за одну итерацию запроса:
Функция ПолучитьПолныйПутьРодителей(СсылкаНаТовар) Экспорт
СписокРодителей = Новый СписокЗначений;
ТекущийЭлемент = СсылкаНаТовар;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Родитель КАК Р1,
| Номенклатура.Родитель.Родитель КАК Р2,
| Номенклатура.Родитель.Родитель.Родитель КАК Р3,
| Номенклатура.Родитель.Родитель.Родитель.Родитель КАК Р4,
| Номенклатура.Родитель.Родитель.Родитель.Родитель.Родитель КАК Р5
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка = &ТекущийЭлемент";
Пока Истина Цикл
Запрос.УстановитьПараметр("ТекущийЭлемент", ТекущийЭлемент);
Выгрузка = Запрос.Выполнить().Выгрузить();
Если Выгрузка.Количество() = 0 Тогда
Прервать;
КонецЕсли;
СтрокаДанных = Выгрузка[0];
Для НомерКолонки = 0 По Выгрузка.Колонки.Количество() - 1 Цикл
РодительУровня = СтрокаДанных[НомерКолонки];
Если НЕ ЗначениеЗаполнено(РодительУровня) Тогда
Прервать;
КонецЕсли;
// Добавляем в начало списка, чтобы соблюсти порядок от корня к элементу
СписокРодителей.Вставить(0, РодительУровня.Наименование);
ТекущийЭлемент = РодительУровня;
КонецЦикла;
// Если мы не заполнили последнюю колонку (Р5), значит дошли до корня
Если НЕ ЗначениеЗаполнено(ТекущийЭлемент) Или Выгрузка.Колонки.Количество() < 5 Тогда
Прервать;
КонецЕсли;
КонецЦикла;
// Собираем массив наименований в единую строку с разделителем
МассивРодителей = СписокРодителей.ВыгрузитьЗначения();
Возврат СтрСоединить(МассивРодителей, " / ");
КонецФункции
Этот метод гарантирует минимальное количество обращений к СУБД даже при экстремальной глубине вложенности справочника.
---
Существует интересный и быстрый "лайфхак" для конфигураций, в которых коды номенклатуры уникальны в пределах подчинения (иерархический код).
Платформенный метод Ссылка.ПолныйКод() возвращает строку, состоящую из кодов всех родителей, разделенных косой чертой, например: "00-000001/00-000014/00-000251". Мы можем использовать этот метод, чтобы быстро получить список родителей за один легкий запрос к базе данных.
Рассмотрим, как реализовать этот алгоритм:
ПолныйКодЭлемента = СсылкаНаТовар.ПолныйКод();."/".Посмотрим на пример реализации данного метода:
Функция ПолучитьПутьЧерезПолныйКод(СсылкаНаТовар) Экспорт
ПолныйКодСтрока = СсылкаНаТовар.ПолныйКод();
// Исключаем код самого элемента (последний сегмент)
МассивКодов = СтрРазделить(ПолныйКодСтрока, "/");
Если МассивКодов.Количество() <= 1 Тогда
Возврат ""; // Родителей нет, элемент лежит в корне
КонецЕсли;
// Удаляем последний элемент (это код самого товара, а не родителя)
МассивКодов.Удалить(МассивКодов.Количество() - 1);
// Запросом получаем наименования для этих кодов, чтобы сохранить правильный порядок
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Код В (&МассивКодов)
| И Номенклатура.ЭтоГруппа";
Запрос.УстановитьПараметр("МассивКодов", МассивКодов);
ТЗРезультат = Запрос.Выполнить().Выгрузить();
// Сопоставляем наименования с исходным порядком кодов
МассивИмен = Новый Массив;
Для Каждого ТекущийКод Из МассивКодов Цикл
СтрокаПоиска = ТЗРезультат.Найти(ТекущийКод, "Код");
Если СтрокаПоиска <> Неопределено Тогда
МассивИмен.Добавить(СтрокаПоиска.Наименование);
КонецЕсли;
КонецЦикла;
Возврат СтрСоединить(МассивИмен, " / ");
КонецФункции
Важное примечание: данный лайфхак будет работать корректно только в том случае, если коды в вашем справочнике уникальны во всей базе данных или настроена автонумерация в пределах подчинения.
---
Если вы выводите данные в отчет с помощью СКД, вам вообще не нужно писать сложный программный код или громоздкие запросы с ВЫБОР КОГДА. Платформа 1С умеет автоматически оперировать иерархическими связями.
Проанализируем два изящных способа настройки СКД:
В схеме компоновки данных можно настроить связь набора данных "сам с собой":
&Родитель выбирает информацию о родительской группе.Родитель, в качестве приемника — Ссылка.В СКД встроен мощный язык выражений. Мы можем добавить вычисляемое поле (например, ПолныйПуть) и использовать в нем выражение с параметром поиска по иерархии:
// Пример выражения для вычисляемого поля в СКД
ВычислитьВыражение("Представление(Родитель)", , "Иерархия", "Первая", "Текущая")
Это выражение автоматически соберет коллекцию представлений всех вышестоящих родителей для текущей строки отчета и объединит их. Нам останется лишь настроить оформление поля.
---
ВЫБОР КОГДА и проверкой на пустую ссылку).