Как сделать индексы для ДанныеФормыКоллекция и ускорить поиск на управляемой форме 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Разработчики управляемых форм в 1С часто сталкиваются с неприятным эффектом: при работе с таблицами, содержащими сотни или тысячи строк, стандартные методы поиска начинают вызывать ощутимые задержки (фризы) интерфейса. Пользователь кликает по строке, система "задумывается" на полсекунды, и только потом данные обновляются. Сегодня мы подробно разберем, почему стандартный метод НайтиСтроки для объекта ДанныеФормыКоллекция может работать медленно, как устроена физика этого процесса в платформе "1С:Предприятие", и рассмотрим эффективные архитектурные решения для создания аналогов индексов на стороне клиента.

В чем причина зависания и как работает ленивая загрузка (Lazy Loading)

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

В платформе 1С реализован механизм оптимизации порционной передачи данных (так называемая ленивая загрузка). Когда форма открывается, сервер передает на клиентский компьютер не всю таблицу целиком, а лишь небольшую стартовую порцию данных (обычно около 20–45 строк), необходимую для отображения текущего видимого экрана. Остальные строки подтягиваются с сервера динамически по мере того, как пользователь прокручивает таблицу мышкой.

Когда мы программно вызываем метод НайтиСтроки на клиенте, платформе требуется гарантировать точный результат поиска по всей коллекции. Понимая, что на клиенте присутствует только часть строк, платформа совершает неявный (скрытый) контекстный вызов сервера, чтобы принудительно и экстренно скачать абсолютно все строки таблицы в память клиента. Именно этот синхронный сетевой запрос на получение сотен строк и вызывает тот самый первоначальный лаг (от 0.5 до 1.5 секунд).

После того как все данные один раз были полностью загружены на клиент, последующие вызовы метода НайтиСтроки отрабатывают практически мгновенно (за несколько миллисекунд), так как данные уже кэшированы в локальной памяти. Но при любом изменении данных или повторном открытии формы процесс "зависания" повторяется.

Критическая ошибка: попытка выгрузки в Таблицу Значений на клиенте

В процессе обсуждения подобных проблем на форумах часто можно встретить совет: "Выгрузите коллекцию формы в обычную Таблицу Значений, проиндексируйте ее и ищите уже по ней". Рассмотрим, почему этот совет технически не реализуем на клиенте.

Объект ТаблицаЗначений является исключительно серверным объектом. Он физически не существует в контексте тонкого, веб- или мобильного клиента. Метод Выгрузить() у объекта типа ДанныеФормыКоллекция недоступен на клиенте. Попытка его вызова:


// Данный код вызовет ошибку выполнения на клиенте!
ОбычнаяТЗ = ТЗНаФорме.Выгрузить(); 

неизбежно приведет к аварийному завершению работы интерфейса с ошибкой: "Метод недоступен на клиенте". Чтобы выгрузить коллекцию в ТЗ, нам придется передать контекст формы на сервер через метод &НаСервере, выполнить выгрузку там, проиндексировать, найти строки и вернуть результат обратно на клиент. Подобное "путешествие" контекста формы весом в сотни строк через сеть сведет на нет всю оптимизацию.

Решение №1. Создание быстрого индекса на клиенте через Соответствие и Идентификаторы

Если нам действительно необходимо осуществлять частый и мгновенный поиск по нескольким колонкам прямо на клиенте без обращений к серверу, мы можем самостоятельно построить индекс. В качестве структуры индекса идеально подходит объект Соответствие, который работает на клиенте чрезвычайно быстро (на основе хэш-таблиц).

Идея метода заключается в следующем: мы сопоставляем уникальный поисковый ключ строки с числовым идентификатором строки в коллекции формы. У каждой строки коллекции ДанныеФормыКоллекция есть внутренний неизменяемый идентификатор, который можно получить методом ПолучитьИдентификатор(). Зная этот идентификатор, мы можем мгновенно получить саму строку коллекции с помощью сверхбыстрого клиентского метода НайтиПоИдентификатору(), который не инициирует сканирование всей таблицы.

Разберем по шагам, как реализовать этот алгоритм. Создадим процедуру индексации таблицы, которую будем вызывать один раз при заполнении данных (например, после получения данных с сервера):


&НаКлиенте
Перем мИндексТаблицы; // Локальная переменная модуля формы для хранения индекса

&НаКлиенте
Процедура ПроиндексироватьТаблицуФормы()
    
    // Инициализируем соответствие для индекса
    мИндексТаблицы = Новый Соответствие;
    
    // Обходим коллекцию формы. На данном этапе мы принудительно "прогреваем" кэш клиента
    Для Каждого СтрокаКоллекции Из Объект.Товары Цикл
        
        // Формируем поисковый ключ. Для составного индекса объединяем значения ключевых колонок
        КлючПоиска = Строка(СтрокаКоллекции.Номенклатура.УникальныйИдентификатор()) 
                     + "_" + Строка(СтрокаКоллекции.Характеристика.УникальныйИдентификатор());
                     
        // Получаем уникальный идентификатор строки формы
        ИдентификаторСтроки = СтрокаКоллекции.ПолучитьИдентификатор();
        
        // Записываем связь "Ключ -> Идентификатор" в наш индекс
        мИндексТаблицы.Вставить(КлючПоиска, ИдентификаторСтроки);
        
    КонецЦикла;
    
КонецПроцедуры

Теперь рассмотрим, как будет выглядеть процедура быстрого поиска строки по нашему индексу на клиенте. Этот код будет отрабатывать за доли миллисекунды, гарантируя отсутствие лагов в интерфейсе:


&НаКлиенте
Функция НайтиСтрокуПоИндексу(ЗначениеНоменклатура, ЗначениеХарактеристика)
    
    // Если индекс еще не создан, создаем его
    Если мИндексТаблицы = Неопределено Тогда
        ПроиндексироватьТаблицуФормы();
    КонецЕсли;
    
    // Формируем ключ для поиска в соответствии
    КлючПоиска = Строка(ЗначениеНоменклатура.УникальныйИдентификатор()) 
                 + "_" + Строка(ЗначениеХарактеристика.УникальныйИдентификатор());
                 
    // Ищем идентификатор строки в хэш-таблице (работает за O(1))
    ИдентификаторСтроки = мИндексТаблицы.Получить(КлючПоиска);
    
    Если ИдентификаторСтроки <> Неопределено Тогда
        // Мгновенно получаем строку по ее идентификатору
        Возврат Объект.Товары.НайтиПоИдентификатору(ИдентификаторСтроки);
    КонецЕсли;
    
    Возврат Неопределено;
    
КонецФункции

Важное примечание: Если пользователь может интерактивно добавлять, удалять или редактировать строки в таблице на форме, то наш индекс (переменная мИндексТаблицы) станет неактуальным. В таких случаях необходимо обновлять индекс при изменении ключевых полей в событиях таблицы ПриИзменении, ПередУдалением или ПриОкончанииРедактирования.

Решение №2. Использование свойства ОтборСтрок для связанных таблиц (без программного поиска)

Очень часто поиск строк на клиенте требуется для реализации классического интерфейса "Мастер-Деталь" (связанные таблицы). Например, когда пользователь щелкает по строке в верхней таблице (группы или общие данные), в нижней таблице должна мгновенно отображаться детальная информация, отфильтрованная по ключевому полю.

Если ваша задача совпадает с этим сценарием, то программно искать строки через НайтиСтроки или циклы вообще не нужно! Платформа управляемых форм предоставляет мощный встроенный инструмент — свойство элемента формы ОтборСтрок. Этот отбор применяется непосредственно на клиенте к визуальному отображению данных, работает мгновенно и не требует никаких серверных вызовов.

Рассмотрим, как это реализовать. Предположим, у нас есть две таблицы на форме: ТаблицаОрганизаций и связанная с ней ТаблицаПодразделений. В событии активизации строки главной таблицы мы пишем следующий лаконичный код:


&НаКлиенте
Процедура ТаблицаОрганизацийПриАктивацииСтроки(Элемент)
    
    ТекущиеДанныеГлавной = Элементы.ТаблицаОрганизаций.ТекущиеДанные;
    
    Если ТекущиеДанныеГлавной <> Неопределено Тогда
        
        // Создаем фиксированную структуру для отбора
        ПараметрыОтбора = Новый Структура("ОрганизацияВладелец", ТекущиеДанныеГлавной.Ссылка);
        ФиксированныйОтбор = Новый ФиксированнаяСтруктура(ПараметрыОтбора);
        
        // Накладываем отбор на подчиненную таблицу формы
        Элементы.ТаблицаПодразделений.ОтборСтрок = ФиксированныйОтбор;
        
    Иначе
        
        // Сбрасываем отбор, если строка не выбрана
        Элементы.ТаблицаПодразделений.ОтборСтрок = Неопределено;
        
    КонецЕсли;
    
КонецПроцедуры

Использование свойства ОтборСтрок имеет колоссальные преимущества:

  1. Мгновенная реакция интерфейса: Платформа оптимизирует скрытие строк на уровне рендеринга операционной системы, не перебирая физические строки коллекции в цикле.
  2. Чистота данных: Сама коллекция данных в форме остается неизменной. Нам не нужно удалять или добавлять строки программно.
  3. Отсутствие серверных вызовов: Все операции происходят локально на клиенте в рамках отрисовки формы.

Решение №3. Хранение таблицы во Временном Хранилище (для больших объемов данных)

Если коллекция на форме содержит не 700 строк, а десятки тысяч, то передавать ее целиком на клиент — это плохая архитектурная практика. Форма будет долго открываться и потреблять огромный объем оперативной памяти на компьютере пользователя. В таких сценариях лучшим решением будет хранение проиндексированной таблицы значений на сервере во Временном Хранилище, а на клиент передавать только результаты поиска через быстрые неконтекстные вызовы.

Проанализируем этот паттерн по шагам:

  1. При первоначальном формировании данных на сервере мы выгружаем их в полноценную ТаблицаЗначений.
  2. Добавляем необходимые индексы к таблице на сервере с помощью метода Индексы.Добавить(). Это гарантирует поиск за микросекунды на уровне платформы.
  3. Помещаем таблицу во временное хранилище и сохраняем его адрес в реквизите формы (строкового типа).
  4. При необходимости выполнить поиск на клиенте, мы делаем легкий неконтекстный вызов сервера (метод с директивой &НаСервереБезКонтекста), передавая туда только адрес хранилища и параметры поиска.
  5. На сервере извлекаем ТЗ, быстро находим нужные данные по индексу и возвращаем клиенту компактную структуру или массив результатов.

Посмотрим на практическую реализацию этого метода. Создадим реквизит управляемой формы АдресХранилищаТаблицы (тип Строка) и напишем следующий код:


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

&НаСервереБезКонтекста
Функция НайтиЦенуНаСервереБезКонтекста(Знач АдресХранилища, Знач Номенклатура, Знач Характеристика)
    
    // Получаем таблицу из хранилища
    ТЗДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
    
    Если ТипЗнч(ТЗДанные) = Тип("ТаблицаЗначений") Тогда
        
        // Быстрый поиск по индексу на сервере
        ПараметрыОтбора = Новый Структура("Номенклатура, Характеристика", Номенклатура, Характеристика);
        НайденныеСтроки = ТЗДанные.НайтиСтроки(ПараметрыОтбора);
        
        Если НайденныеСтроки.Количество() > 0 Тогда
            Возврат НайденныеСтроки[0].Цена;
        КонецЕсли;
        
    КонецЕсли;
    
    Возврат 0;
    
КонецФункции

&НаКлиенте
Процедура ПолучитьИнформациюОЦене()
    
    ТекущиеДанные = Элементы.Товары.ТекущиеДанные;
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Выполняем быстрый неконтекстный вызов, передавая только ссылку
    Цена = НайтиЦенуНаСервереБезКонтекста(АдресХранилищаТаблицы, ТекущиеДанные.Номенклатура, ТекущиеДанные.Характеристика);
    
    // Применяем результат на клиенте
    ТекущиеДанные.Цена = Цена;
    
КонецПроцедуры

Этот метод полностью решает проблему "тяжелых" форм. Клиентское приложение не загружается лишними мегабайтами данных, форма открывается мгновенно, а поиск по индексам на сервере через неконтекстный вызов происходит быстрее, чем передача контекста всей формы по сети.

Резюме и рекомендации разработчику

Для выбора оптимальной стратегии ускорения поиска в коллекциях управляемых форм мы рекомендуем придерживаться следующего алгоритма:

← На главную