Работа с иерархическими структурами данных в платформе «1С:Предприятие 8» — стандартная задача для разработчика. Однако при переходе на управляемое приложение многие сталкиваются с лавинообразным падением производительности при выводе деревьев значений на форму. Казалось бы, скромная выборка из 6 000 строк в дереве может загружаться долгие минуты, полностью блокируя интерфейс пользователя.
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутренние механизмы платформы и рассмотрим самые эффективные способы оптимизации, включая технологию «ленивой загрузки» (Lazy Loading), оптимизацию типов данных и отказ от избыточных вызовов серверного контекста.
Давайте проанализируем классический код, который часто пишут разработчики и который приводит к сильным зависаниям интерфейса:
&НаСервере
Процедура ЗаполнитьДеревоНаСервере()
РезультатЗапроса = Запрос.Выполнить();
Дерево = РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
ЭтаФорма.ЗначениеВРеквизитФормы(Дерево, "Дерево");
КонецПроцедуры
На первый взгляд, все сделано по стандартам. Но именно в строке вызова метода ЗначениеВРеквизитФормы() кроется главная проблема. Давайте разберем по шагам, что происходит «под капотом» платформы при выполнении этого метода:
ДеревоЗначений, находящийся в оперативной памяти сервера, не может быть напрямую передан на клиент. Платформа должна преобразовать его в специальный внутренний XML/JSON формат (структуру ДанныеФормыДерево).Для оптимизации этого процесса нам необходимо кардинально изменить подход к заполнению и передаче данных. Рассмотрим пошагово ключевые методики ускорения работы.
Это самый мощный и рекомендуемый способ оптимизации иерархических структур. Его суть заключается в том, чтобы не загружать на клиент всю базу сразу. Мы заполняем только самый верхний (первый) уровень дерева. Если у элемента есть подчиненные элементы, мы создаем для него одну пустую строку-маркер («плюсик»). Когда пользователь нажимает на этот «плюсик», система перехватывает событие и динамически догружает подчиненные строки только для этого узла.
Рассмотрим детальную реализацию этого алгоритма на примере иерархического справочника номенклатуры.
Шаг 1. Первоначальное заполнение корня дерева при открытии формы.
При инициализации формы мы делаем быстрый запрос, который выбирает только элементы верхнего уровня (у которых родитель пустой).
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ИнициализироватьПервыйУровеньДерева();
КонецПроцедуры
&НаСервере
Процедура ИнициализироватьПервыйУровеньДерева()
// Запрос выбирает только элементы верхнего уровня
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.ЭтоГруппа КАК ЭтоГруппа
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Родитель = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
// Получаем коллекцию элементов дерева напрямую, без использования ЗначениеВРеквизитФормы
КоллекцияЭлементов = РеквизитФормыВЗначение("Дерево").Строки;
КоллекцияЭлементов.Очистить();
Пока Выборка.Следующий() Цикл
НоваяСтрока = КоллекцияЭлементов.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
// Если это группа, добавляем пустую строку-маркер
Если Выборка.ЭтоГруппа Тогда
СтрокаМаркер = НоваяСтрока.Строки.Добавить();
СтрокаМаркер.Наименование = "Загрузка..."; // Служебный маркер
КонецЕсли;
КонецЦикла;
// Возвращаем объект обратно в реквизит формы
ЗначениеВРеквизитФормы(КоллекцияЭлементов.Владелец(), "Дерево");
КонецПроцедуры
Шаг 2. Перехват раскрытия ветки на клиенте.
Для таблицы на форме определяем событие ПередРазворачиванием. Здесь мы проверим, загружены ли данные, или там все еще находится наш маркер «Загрузка...».
&НаКлиенте
Процедура ДеревоПередРазворачиванием(Элемент, СтрокаИД, Отказ)
ТекущаяСтрока = Дерево.НайтиПоИдентификатору(СтрокаИД);
ПодчиненныеЭлементы = ТекущаяСтрока.ПолучитьЭлементы();
// Проверяем, содержит ли узел наш маркер автозагрузки
Если ПодчиненныеЭлементы.Количество() = 1 И ПодчиненныеЭлементы[0].Наименование = "Загрузка..." Тогда
// Вызываем серверную процедуру для динамической подгрузки реальных данных
ПодгрузитьПодчиненныеСтрокиНаСервере(СтрокаИД);
КонецЕсли;
КонецПроцедуры
Шаг 3. Динамическая подгрузка ветки на сервере.
Обратите внимание: мы не конвертируем все дерево через ЗначениеВРеквизитФормы(). Мы точечно находим элемент формы на сервере через коллекцию ДанныеФормыДерево, удаляем маркер и добавляем новые строки. Это работает мгновенно!
&НаСервере
Процедура ПодгрузитьПодчиненныеСтрокиНаСервере(СтрокаИД)
// Находим строку дерева непосредственно в реквизите формы по ее идентификатору
РодительскийЭлемент = Дерево.НайтиПоИдентификатору(СтрокаИД);
КоллекцияПодчиненных = РодительскийЭлемент.ПолучитьЭлементы();
// Удаляем строку-маркер "Загрузка..."
КоллекцияПодчиненных.Очистить();
// Получаем реальные данные из БД только для этого родителя
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.ЭтоГруппа КАК ЭтоГруппа
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Родитель = &Родитель";
Запрос.УстановитьПараметр("Родитель", РодительскийЭлемент.Ссылка);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
НоваяСтрока = КоллекцияПодчиненных.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
// Если внутри новой строки тоже есть группы, вешаем маркер для дальнейшего раскрытия
Если Выборка.ЭтоГруппа Тогда
СтрокаМаркер = НоваяСтрока.ПолучитьЭлементы().Добавить();
СтрокаМаркер.Наименование = "Загрузка...";
КонецЕсли;
КонецЦикла;
КонецПроцедуры
При использовании этого метода даже дерево с миллионом позиций будет открываться на форме за миллисекунды, так как единовременно в памяти клиента будет находиться ничтожно малый объем данных.
Если специфика вашей задачи требует вывода всего объема данных сразу, проанализируем структуру колонок самого дерева. Главное правило оптимизации в управляемых формах: чем проще типы данных в колонках, тем быстрее выполняется сериализация.
Давайте разберем практические шаги по оптимизации колонок:
Строка(Ссылка.УникальныйИдентификатор()). Восстанавливайте ссылку на сервере только в момент клика или выбора строки пользователем. Строковые операции сериализуются платформой в разы быстрее, чем объектные метаданные ссылок.Если в вашей конфигурации установлена Библиотека стандартных подсистем (БСП), то разработчики фирмы «1С» уже позаботились об эффективных методах заполнения реквизитов формы. Для быстрого переноса данных используйте функции общего назначения БСП.
В частности, метод ОбщегоНазначения.ЗаполнитьКоллекциюЭлементовДереваДанныхФормы() выполняет перенос данных из объекта ДеревоЗначений в реквизит формы типа ДанныеФормыДерево по максимально оптимизированным внутренним алгоритмам платформы, минимизируя накладные расходы.
Пример использования функции БСП:
&НаСервере
Процедура ЗаполнитьЧерезБСП(ДеревоЗначенийИсточник)
// Быстрое заполнение реквизита "Дерево" на основе подготовленного дерева значений
ОбщегоНазначения.ЗаполнитьКоллекциюЭлементовДереваДанныхФормы(ДеревоЗначенийИсточник, Дерево);
КонецПроцедуры
Часто перед заполнением или выводом дерева разработчики выполняют его предварительную обработку на сервере (например, рассчитывают итоги, устанавливают картинки состояния, проставляют флаги). Традиционно для этого используется рекурсивный вызов функций. Однако на больших деревьях рекурсия создает огромную нагрузку на стек вызовов платформы и снижает общую скорость обработки.
Рекомендуем заменить рекурсию итеративным обходом с использованием массива в качестве стека. Посмотрим на этот пример:
&НаСервере
Процедура ОбработатьДеревоИтеративно(ДеревоЗначений)
// Создаем массив, который будет выступать в роли стека
СтекСтрок = Новый Массив;
// Помещаем в стек все корневые строки дерева
Для Каждого КорневаяСтрока Из ДеревоЗначений.Строки Цикл
СтекСтрок.Добавить(КорневаяСтрока);
КонецЦикла;
// Обрабатываем элементы, пока стек не опустеет
Пока СтекСтрок.Количество() > 0 Цикл
// Извлекаем последний элемент из стека (LIFO)
ТекущийИндекс = СтекСтрок.Количество() - 1;
ТекущаяСтрока = СтекСтрок[ТекущийИндекс];
СтекСтрок.Удалить(ТекущийИндекс);
// Выполняем полезную обработку строки
ТекущаяСтрока.Наименование = ВРег(ТекущаяСтрока.Наименование);
// Помещаем подчиненные строки текущего элемента в стек для дальнейшей обработки
Для Каждого ДочерняяСтрока Из ТекущаяСтрока.Строки Цикл
СтекСтрок.Добавить(ДочерняяСтрока);
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Этот подход гарантирует стабильность приложения, исключает риски переполнения стека и работает на 30–50% быстрее классического рекурсивного обхода дерева.
Важно помнить фундаментальное правило разработки в управляемом приложении 1С: интерфейс формы предназначен исключительно для визуализации информации, а не для расчетов.
Если вам требуется дерево значений для проведения математических расчетов, распределения сумм или анализа данных — ни в коем случае не выполняйте эти операции непосредственно в данных формы. Давайте организуем процесс правильно:
ТаблицаЗначений, Соответствие или во временных таблицах СУБД.Соблюдение этих правил позволит вам создавать отзывчивые и быстрые интерфейсы даже при работе с очень большими объемами иерархических данных.