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