При разработке в среде «1С:Предприятие 8» (особенно на управляемых формах) начинающие программисты часто сталкиваются с трудностями при передаче данных между клиентом и сервером. Типичный сценарий: нужно получить какие-то расчетные данные с сервера (например, готовность проектов, цены, остатки) и быстро распределить их по строкам табличной части на клиенте.
В этой статье мы подробно разберем, почему стандартный метод Найти() может не работать при поиске в массиве, проанализируем неэффективные алгоритмы поиска типа «петля в петле» (вложенные циклы) и рассмотрим два профессиональных решения этой задачи: использование объекта Соответствие для быстрого поиска на клиенте со сложностью O(1) и перенос всей логики обработки в контекст сервера.
Проанализируем ситуацию, описанную автором темы. На сервере в безконтекстной процедуре формируется коллекция данных (процент готовности проектов по номенклатуре или продукции). Эта коллекция передается на клиент. Автор называет эту коллекцию «массивом с колонками», что сразу вызывает путаницу. В 1С у объекта типа Массив не может быть колонок — колонки есть у ТаблицыЗначений. Однако таблицу значений нельзя напрямую передать с сервера на клиент в управляемом приложении.
Чтобы обойти это ограничение, разработчики часто используют массив структур. В этом случае на сервере создается массив, элементами которого являются структуры со свойствами (например, Наименование и ПроцентГотовности). Этот массив структур отлично сериализуется и передается на клиент.
Посмотрим на ошибочную попытку поиска в таком массиве на клиенте:
Для каждого ТекСтр из Объект.Спецификация цикл
ПолученнаяСтрока = ТабЧастьГотовностьПроекта.Найти(ТекСтр.ПродукцияИлиМатериал);
// Попытка найти значение в массиве структур
КонецЦикла;
Выясним причину, почему данный код не работает. Метод Массив.Найти() предназначен для поиска точного совпадения элемента массива. Он обходит элементы массива и сравнивает каждый из них с переданным параметром (в нашем случае это ссылка ТекСтр.ПродукцияИлиМатериал). Но элементами нашего массива являются объекты типа Структура!
Платформа 1С пытается сравнить объект Структура со ссылкой на справочник. Такое сравнение всегда возвращает Ложь. Метод Найти() просто «не знает», что вы хотите заглянуть внутрь структуры и сравнить одно из ее свойств с вашей ссылкой. Именно поэтому метод возвращает Неопределено (или индекс -1), а поиск завершается неудачей.
В качестве временного решения автор применил следующий алгоритм:
Для каждого ТекСтр из Объект.Спецификация цикл
Для каждого ТекСтрМассива из ТабЧастьГотовностьПроекта цикл
Если ТекСтр.ПродукцияИлиМатериал = ТекСтрМассива.Наименование тогда
ТекСтр.ПроцентВыполнения = ТекСтрМассива.ПроцентГотовности
КонецЕсли;
КонецЦикла;
КонецЦикла;
Давайте проанализируем эту конструкцию. Перед нами классический пример вложенного цикла со сложностью алгоритма O(N * M), где N — количество строк в табличной части спецификации, а M — количество элементов в массиве результатов.
Если в табличной части документа будет всего 100 строк, а в массиве результатов — 100 элементов, системе придется выполнить 10 000 итераций сравнения! На реальной базе данных при увеличении объема документов такой код приведет к заметному зависанию клиентского интерфейса. Пользователь будет наблюдать «песочные часы» просто при открытии формы или расчете данных. Называть переменные коллекций неподходящими именами (например, использовать имя ТабЧастьГотовностьПроекта для массива) также является плохой практикой разработки, так как это сильно затрудняет чтение и поддержку кода другими специалистами.
Если нам обязательно нужно передать данные с сервера на клиент и там быстро сопоставить их со строками табличной части, лучшим инструментом будет объект типа Соответствие (в других языках программирования его называют Dictionary или Map).
Объект Соответствие хранит данные в формате «Ключ — Значение». В качестве ключа мы можем использовать нашу ссылку на продукцию (ПродукцияИлиМатериал), а в качестве значения — процент готовности. Поиск в соответствии по ключу происходит мгновенно за счет хэш-таблиц (вычислительная сложность O(1)), независимо от размера коллекции.
Разберем по шагам реализацию этого подхода.
Шаг 1. Формирование данных на сервере без контекста:
&НаСервереБезКонтекста
Функция ПолучитьСоответствиеГотовности(СсылкаНаДоговор)
РезультатПоиска = Новый Соответствие;
// Предположим, мы получаем данные из базы с помощью запроса
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ГотовностьПроектов.ПродукцияИлиМатериал КАК ПродукцияИлиМатериал,
| ГотовностьПроектов.ПроцентГотовности КАК ПроцентГотовности
|ИЗ
| РегистрСведений.ГотовностьПроектов КАК ГотовностьПроектов
|ГДЕ
| ГотовностьПроектов.Договор = &Договор";
Запрос.УстановитьПараметр("Договор", СсылкаНаДоговор);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Заполняем соответствие: Ключ - ссылка на продукцию, Значение - процент
РезультатПоиска.Вставить(Выборка.ПродукцияИлиМатериал, Выборка.ПроцентГотовности);
КонецЦикла;
Возврат РезультатПоиска;
КонецФункции
Шаг 2. Быстрое заполнение табличной части на клиенте:
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Получаем соответствие с сервера за один вызов
СоответствиеГотовности = ПолучитьСоответствиеГотовности(Объект.Ссылка);
// Обходим строки табличной части в один цикл
Для каждого ТекСтр Из Объект.Спецификация Цикл
// Получаем значение из соответствия по ключу-ссылке напрямую за ОДНО действие!
НайденныйПроцент = СоответствиеГотовности[ТекСтр.ПродукцияИлиМатериал];
Если НайденныйПроцент <> Неопределено Тогда
ТекСтр.ПроцентВыполнения = НайденныйПроцент;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот алгоритм работает в сотни раз быстрее вложенных циклов. В нем нет лишних сравнений, а код выглядит лаконично и профессионально.
Несмотря на то, что использование Соответствия полностью решает проблему производительности на клиенте, стандарты разработки «1С» рекомендуют минимизировать любые расчеты на стороне клиента. Клиентское приложение должно отвечать исключительно за отображение интерфейса и взаимодействие с пользователем.
Правильнее всего выполнить заполнение табличной части на сервере в контекстном вызове. На сервере нам доступны любые типы данных (включая ТаблицуЗначений), и мы можем выполнить сопоставление данных прямо в памяти сервера или даже одним запросом.
Рассмотрим пример реализации через контекстный вызов сервера:
&НаСервере
Процедура ЗаполнитьГотовностьНаСервере()
// 1. Получаем данные из базы через запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ГотовностьПроектов.ПродукцияИлиМатериал КАК ПродукцияИлиМатериал,
| ГотовностьПроектов.ПроцентГотовности КАК ПроцентГотовности
|ИЗ
| РегистрСведений.ГотовностьПроектов КАК ГотовностьПроектов
|ГДЕ
| ГотовностьПроектов.Договор = &Договор";
Запрос.УстановитьПараметр("Договор", Объект.Ссылка);
// Выгружаем результат во временную Таблицу Значений для быстрого поиска
ТаблицаГотовности = Запрос.Выполнить().Выгрузить();
// Индексируем колонку для моментального поиска методом Найти()
ТаблицаГотовности.Индексы.Добавить("ПродукцияИлиМатериал");
// 2. Обходим коллекцию данных формы на сервере
Для каждого ТекСтр Из Объект.Sпецификация Цикл
// На сервере метод Найти() у проиндексированной ТЗ работает мгновенно
СтрокаИсточника = ТаблицаГотовности.Найти(ТекСтр.ПродукцияИлиМатериал, "ПродукцияИлиМатериал");
Если СтрокаИсточника <> Неопределено Тогда
ТекСтр.ПроцентВыполнения = СтрокаИсточника.ПроцентГотовности;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Вызываем серверную процедуру, которая все сделает сама
ЗаполнитьГотовностьНаСервере();
КонецПроцедуры
Преимущества данного подхода:
ТаблицаГотовности.Индексы.Добавить()) гарантирует максимальное быстродействие поиска данных на стороне сервера.В процессе обсуждения на форуме автор упомянул возможное использование Временного хранилища. Разберем этот момент подробнее.
Временное хранилище в 1С (функции ПоместитьВоВременноеХранилище() и ПолучитьИзВременногоХранилища()) используется для передачи тяжелых, несериализуемых данных (например, файлов, картинок, или полноценных таблиц значений) между различными серверными вызовами в рамках одного сеанса. Также оно незаменимо при передаче данных между фоновыми заданиями и основным потоком.
В нашем случае использовать временное хранилище не требуется и нецелесообразно. Нам нужно заполнить реквизиты формы при ее открытии. Обычный вызов серверного контекста формы (процедура с директивой выполнения &НаСервере) справляется с этой задачей идеально. Использование временного хранилища здесь только перегрузит код лишними строками, усложнит логику отладки и не даст никакого выигрыша в производительности.
Подведем краткий итог нашей совместной работы по оптимизации алгоритма поиска:
Массив.Найти() для поиска по свойствам внутренних объектов (например, структур) — этот метод предназначен для поиска простых значений или точных совпадений самих объектов.Соответствие и выполняйте мгновенный поиск по ключу.СоответствиеГотовности вместо ТабЧастьГотовностьПроекта), чтобы ваш код был понятен коллегам и вам самим спустя некоторое время.