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