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