Три способа получить дерево элементов иерархического справочника

Рассматривается применимость и недостатки следующих способов получения дерева
1) Запрос с использованием итогов по иерархии
2) Формирование дерева обходом выборки с упорядочиванием по иерархии
3) Формирование иерархии по списку элементов транзитивным замыканием

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

Для однозначного понимания моих объяснений введу несколько «терминов», которыми буду пользоваться. Все листья дерева буду называть элементами. Узлы дерева, которые содержат только элементы — нижние папки, Остальные узлы, которые содержат хотя бы одну нижнюю папку — верхние папки.

Для ИЕРАРХИЯ — все логично: разбираем дерево итогов по иерархии для папок. У всех папок тип  — ТипЗаписиЗапроса.ИтогПоИерархии. У элементов тип — ТипЗаписиЗапроса.ИтогПоГруппировке. Внутри группировки одна запись того же элемента но уже с типом  ТипЗаписиЗапроса.ДетальнаяЗапись. Все как заявлено. Но если выгрузить в дерево, дубль пропадает!

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа
// Тут могут быть условия
| |УПОРЯДОЧИТЬ ПО | Номенклатура.ЭтоГруппа, | Ссылка |ИТОГИ ПО | Ссылка ИЕРАРХИЯ |АВТОУПОРЯДОЧИВАНИЕ"; Дерево=Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);

Либо сформировать вручную

ВыборкаСИерархией=Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией,"Ссылка");
Дерево=Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Номенклатура");
ВыбратьЭлементыВИерархии(ВыборкаСИерархией,Дерево);
КонецПроцедуры

Процедура ВыбратьЭлементыВИерархии(ВыборкаСИерархией,Дерево)
Пока ВыборкаСИерархией.Следующий() Цикл
Если ВыборкаСИерархией.ТипЗаписи()=ТипЗаписиЗапроса.ИтогПоИерархии Тогда
Строка=Дерево.Строки.Добавить();
Строка.Номенклатура=ВыборкаСИерархией.Ссылка;
ВыбратьЭлементыВИерархии (ВыборкаСИерархией.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией,"Ссылка"),Строка);
ИначеЕсли ВыборкаСИерархией.ТипЗаписи()=ТипЗаписиЗапроса.ИтогПоГруппировке Тогда
//Тут на самом деле есть еще один уровень, но нам он может потребоваться,
//только если нужно взять данные на уровне записи
//Если это нужно, здесь обходим один элемент, забираем из него данные
//Выборка=ВыборкаСИерархией.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам
//Пока ВыборкаСледующий() Цикл
// Строка=Дерево.Строки.Добавить();
// Строка.Номенклатура=ВыборкаСИерархией.Ссылка;
//КонецЦикла;
Строка=Дерево.Строки.Добавить();
Строка.Номенклатура=ВыборкаСИерархией.Ссылка;
КонецЕсли;
КонецЦикла;
КонецПроцедуры

Для ТОЛЬКО ИЕРАРХИЯ все немного не так, как ожидалось. Верхние папки — ТипЗаписиЗапроса.ИтогПоИерархии, нижние — ТипЗаписиЗапроса.ИтогПоГруппировке. Внутри элементы с типом — ТипЗаписиЗапроса.ДетальнаяЗапись. НО, если верхняя папка содержит элементы, все они будут помещены в еще в одну вложенную группу с типом  ТипЗаписиЗапроса.ИтогПоГруппировке.  Поэтому выгрузить() приводит к дублированию! Цель такого дублирования думаю в том, чтобы все элементы обязательно содержались в папке с ТипЗаписиЗапроса.ИтогПоГруппировке, чтобы мы могли обходить выборку ОбходРезультатаЗапроса.ПоГруппировкам. Поэтому, если использовать ТОЛЬКО ИЕРАРХИЯ, лучше обойти выборку и сформировать ручками дерево, устраняя дублирование.  При обходе нужно обязательно указывать второй параметр «Группировки». Привожу обход для  ТОЛЬКО ИЕРАРХИЯ

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа
// Тут могут быть условия
| |УПОРЯДОЧИТЬ ПО | Номенклатура.ЭтоГруппа, | Ссылка |ИТОГИ ПО | Ссылка ТОЛЬКО ИЕРАРХИЯ |АВТОУПОРЯДОЧИВАНИЕ"; Результат=Запрос.Выполнить(); ВыборкаСИерархией=Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией,"Ссылка"); Дерево=Новый ДеревоЗначений; Дерево.Колонки.Добавить("Номенклатура"); ВыбратьЭлементыВИерархии(ВыборкаСИерархией,Дерево); Процедура ВыбратьЭлементыВИерархии(ВыборкаСИерархией,Дерево) Пока ВыборкаСИерархией.Следующий() Цикл Если ВыборкаСИерархией.ТипЗаписи()=ТипЗаписиЗапроса.ИтогПоИерархии Тогда Строка=Дерево.Строки.Добавить(); Строка.Номенклатура=ВыборкаСИерархией.Ссылка; ВыбратьЭлементыВИерархии (ВыборкаСИерархией.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией,"Ссылка"),Строка); ИначеЕсли ВыборкаСИерархией.ТипЗаписи()=ТипЗаписиЗапроса.ИтогПоГруппировке Тогда Если Не ЗначениеЗаполнено(ВыборкаСИерархией.Ссылка) ИЛИ ТипЗнч(Дерево)=Тип("СтрокаДереваЗначений") И ВыборкаСИерархией.Ссылка=Дерево.Номенклатура Тогда ВыбратьЭлементыБезИерархии(ВыборкаСИерархией.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам),Дерево); Иначе Строка=Дерево.Строки.Добавить(); Строка.Номенклатура=ВыборкаСИерархией.Ссылка; ВыбратьЭлементыБезИерархии(ВыборкаСИерархией.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам),Строка); КонецЕсли; КонецЕсли; КонецЦикла; КонецПроцедуры Процедура ВыбратьЭлементыБезИерархии (ВыборкаБезИерархии,Дерево) Пока ВыборкаБезИерархии.Следующий() Цикл Строка=Дерево.Строки.Добавить(); Строка.Номенклатура=ВыборкаБезИерархии.Ссылка; КонецЦикла; КонецПроцедуры

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

2) Для решения этой проблемы необходимо выбрать папки в запросе. Такой запрос не получиться выгрузить в дерево, но, если мы будем использовать в запросе УПОРЯДОЧИТЬ ПО … ИЕРАРХИЯ, а также выберем в запросе родителя, то обход дерева станет простым, мы будем обходить выборку в цикле и прицеплять следующий элемент к текущему или одному из его родителей. К кому цеплять покажет выбранное поле родитель.

Запрос = Новый Запрос;
Запрос.Текст ="
|ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Родитель КАК Родитель
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
// Тут могут быть условия
| |УПОРЯДОЧИТЬ ПО | Номенклатура.Ссылка ИЕРАРХИЯ |АВТОУПОРЯДОЧИВАНИЕ"; Результат = Запрос.Выполнить(); ДеревоПапок=Новый ДеревоЗначений; ДеревоПапок.Колонки.Добавить("Номенклатура"); Выборка = Результат.Выбрать(); Пока Выборка.Следующий() Цикл Если Не ЗначениеЗаполнено(Выборка.Родитель) Тогда ТекЭлемент=ДеревоПапок.Строки.Добавить(); ТекЭлемент.Номенклатура=Выборка.Ссылка; Иначе Пока ТекЭлемент.Номенклатура<>Выборка.Родитель Цикл ТекЭлемент=ТекЭлемент.Родитель; КонецЦикла; ТекЭлемент=ТекЭлемент.Строки.Добавить(); ТекЭлемент.Номенклатура=Выборка.Ссылка; КонецЕсли; КонецЦикла;

Этот метод хорош, но его невозможно применить, если мы не можем получить сразу всю иерархию. Если у нас есть только набор элементов, нам необходимо предварительно построить всю иерархию.
3) Итак, у нас есть набор элементов (набор условий отбора на элементы), но мы не знаем их родителей, нам нужно их вычислить, а затем сформировать дерево. Тут нам не обойтись без Сергея (ildarovich) (отдельное спасибо ему за качественный контент) и его публикации //infostart.ru/public/158512/

Рассмотрим задачу получения только иерархии по набору элементов. Для решения задачи выберем для элементов все папки, в которых они содержатся, затем замыканием вычислим всех родителей этих папок, ну и далее выборка с обходом по 2 методу.

Запрос = Новый Запрос;
Запрос.Текст =ТранзитивноеЗамыкание(256);

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

ДеревоПапок=Новый ДеревоЗначений;
ДеревоПапок.Колонки.Добавить("Номенклатура");
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Если Не ЗначениеЗаполнено(Выборка.Родитель) Тогда
ТекЭлемент=ДеревоПапок.Строки.Добавить();
ТекЭлемент.Номенклатура=Выборка.Ссылка;
Иначе
Пока ТекЭлемент.Номенклатура<>Выборка.Родитель Цикл
ТекЭлемент=ТекЭлемент.Родитель;
КонецЦикла;
ТекЭлемент=ТекЭлемент.Строки.Добавить();
ТекЭлемент.Номенклатура=Выборка.Ссылка;
КонецЕсли;
КонецЦикла;

Функция ТранзитивноеЗамыкание(МаксимальнаяДлинаПути)
//Эмуляция отбора элементов. Выборка Папок, в которых они содержаться, затем замыкаем
Пролог = "ВЫБРАТЬ Ссылка ПОМЕСТИТЬ ВТ_Элементы ИЗ Справочник.Номенклатура КАК Номенклатура ГДЕ НЕ ЭтоГруппа;
|ВЫБРАТЬ РАЗЛИЧНЫЕ Ссылка.Родитель КАК Ссылка, Ссылка.Родитель.Родитель КАК Родитель ПОМЕСТИТЬ ВТ_Папки ИЗ ВТ_Элементы КАК ВТ_Элементы;
|ВЫБРАТЬ Родитель НачалоДуги, Ссылка КонецДуги ПОМЕСТИТЬ ЗамыканияДлины1 ИЗ ВТ_Папки
| ГДЕ Родитель <> Значение(Справочник.Номенклатура.ПустаяСсылка)
| ОБЪЕДИНИТЬ ВЫБРАТЬ Ссылка, Ссылка ИЗ ВТ_Папки;";
Рефрен = "ВЫБРАТЬ РАЗЛИЧНЫЕ ПерваяДуга.НачалоДуги, ВтораяДуга.КонецДуги ПОМЕСТИТЬ ЗамыканияДлины#2 ИЗ ЗамыканияДлины#1 КАК ПерваяДуга
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ЗамыканияДлины#1 КАК ВтораяДуга ПО ПерваяДуга.КонецДуги = ВтораяДуга.НачалоДуги;
| УНИЧТОЖИТЬ ЗамыканияДлины#1;";
Эпилог = "ВЫБРАТЬ РАЗЛИЧНЫЕ НачалоДуги КАК Ссылка ПОМЕСТИТЬ ВТ_ВсеПапки ИЗ ЗамыканияДлины#2 ГДЕ НачалоДуги<>Значение(Справочник.Номенклатура.ПустаяСсылка);
|ВЫБРАТЬ Ссылка, Ссылка.Родитель КАК Родитель ИЗ ВТ_ВсеПапки УПОРЯДОЧИТЬ ПО Ссылка ИЕРАРХИЯ АВТОУПОРЯДОЧИВАНИЕ";
ТекстЗапроса = Пролог;
МаксимальнаяДлинаЗамыканий = 1;
Пока МаксимальнаяДлинаЗамыканий < МаксимальнаяДлинаПути Цикл
ТекстЗапроса = ТекстЗапроса + СтрЗаменить(СтрЗаменить(Рефрен, "#1", Формат(МаксимальнаяДлинаЗамыканий, "ЧГ=0")), "#2", Формат(2 * МаксимальнаяДлинаЗамыканий, "ЧГ=0"));
МаксимальнаяДлинаЗамыканий = 2 * МаксимальнаяДлинаЗамыканий
КонецЦикла;
ТекстЗапроса = ТекстЗапроса + СтрЗаменить(Эпилог, "#2", Формат(МаксимальнаяДлинаЗамыканий, "ЧГ=0"));
Возврат ТекстЗапроса;
КонецФункции

9 Comments

  1. echo77

    Есть еще один способ, который, ИМХО, является более громоздким, но работает быстрее, т.к. СКД выгружает в дерево значений быстрее.

    Это тот же запрос в СКД и настройка иерархической группировки

    Reply
  2. 32ops

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

    Reply
  3. echo77
    Reply
  4. 32ops

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

    Reply
  5. Max.Potapov

    Есть более интересное и простое решение на СКД — http://start1c.blogspot.ru/2017/01/blog-post.html

    Reply
  6. Дейл

    Просто, ради интереса. А как построить дерево , если есть к примеру только два поля «Артикул» и «АртикулРодитель». Оба «Строка». Иерархии не доступны.

    Reply
  7. Дейл

    Что-то близкое к 3 варианту, но…

    Reply
  8. Mart

    Супер! Признаться, я и не знал, что упорядочивать можно не только по возрастанию/убыванию, но еще и по иерархии. Спасибо, для меня это как раз то, что нужно.

    Reply
  9. echo77

    (0)

    Запрос = Новый Запрос;
    Запрос.Текст =
    «ВЫБРАТЬ
    |    Номенклатура.Ссылка КАК Ссылка
    |ИЗ
    |    Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |    НЕ Номенклатура.ЭтоГруппа
    // Тут могут быть условия
    |
    |УПОРЯДОЧИТЬ ПО
    |    Номенклатура.ЭтоГруппа,
    |    Ссылка
    |ИТОГИ ПО
    |    Ссылка ИЕРАРХИЯ
    |АВТОУПОРЯДОЧИВАНИЕ»;
    Дерево=Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);

    Показать

    При выгрузке в дерево так же двоит элементы

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *