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