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