Как оптимально вывести колонку «Оплачено» в списке документов на обычных формах 1С без зависаний?

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

При работе с обычными формами в 1С:Предприятие 8 разработчики часто сталкиваются с задачей отображения дополнительных расчетных данных в списках документов. Классический пример — колонка «Оплачено» в списке накладных (документов реализации). Эта колонка должна динамически рассчитываться: если к текущей реализации привязаны документы оплаты (например, Приходный кассовый ордер или Поступление на расчетный счет), выводится значение Истина (или галочка), иначе — Ложь.

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

В чем причина зависаний: событие ПриВыводеСтроки против ПриПолученииДанных

Для вывода кастомных данных в табличных полях обычных форм разработчики обычно выбирают между двумя событиями: ПриВыводеСтроки и ПриПолученииДанных. Выясним причину, почему первый вариант категорически не подходит для тяжелых расчетов, а второй требует правильного паттерна использования.

  1. Событие ПриВыводеСтроки. Вызывается платформой индивидуально для каждой строки списка, которая в данный момент отрисовывается на экране. Если на экране отображается 30 строк, событие выполнится 30 раз. Если внутри обработчика написать обращение к базе данных (через точку от ссылки или посредством запроса), платформа выполнит 30 отдельных SQL-запросов к СУБД (проблема N+1 запроса). Скроллинг списка при этом превратится в кошмар для пользователя.
  2. Событие ПриПолученииДанных. Вызывается один раз для всей пачки строк (порции данных), которые платформа подготовила к выводу на экран. В параметре ОформленияСтрок передается коллекция, содержащая оформление сразу всех видимых строк (обычно от 15 до 45 штук). Это дает нам возможность собрать все ссылки документов одной пачкой, выполнить всего один запрос к базе данных и быстро распределить результаты по строкам.

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


// НЕПРАВИЛЬНО: Запрос внутри цикла в событии ПриПолученииДанных
Для Каждого ОформлениеСтроки Из ОформленияСтрок Цикл
    СсылкаНаДокумент = ОформлениеСтроки.ДанныеСтроки.Ссылка;
    
    // Каждый проход цикла порождает обращение к СУБД!
    ЕстьОплата = ПроверитьОплатуПоДокументу(СсылкаНаДокумент); 
    ОформлениеСтроки.Ячейки.Оплачено.УстановитьФлажок(ЕстьОплата);
КонецЦикла;

Давайте разберем по шагам, как переписать этот алгоритм на правильный, пакетный метод обработки данных.

Решение 1. Оптимальный пакетный запрос в событии ПриПолученииДанных

Чтобы избежать многократных обращений к СУБД, мы должны применить следующий паттерн:

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

Рассмотрим подробнее программную реализацию этого алгоритма:


Процедура ДокументСписокПриПолученииДанных(Элемент, ОформленияСтрок)
    
    // Шаг 1: Собираем массив уникальных ссылок на документы, отображаемые на экране
    МассивДокументов = Новый Массив;
    Для Каждого ОформлениеСтроки Из ОформленияСтрок Цикл
        // Проверяем, что в строке действительно находятся данные документа
        Если ОформлениеСтроки.ДанныеСтроки = Неопределено Тогда
            Продолжить;
        КонецЕсли;
        
        МассивДокументов.Добавить(ОформлениеСтроки.ДанныеСтроки.Ссылка);
    КонецЦикла;
    
    Если МассивДокументов.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // Шаг 2: Выполняем ОДИН пакетный запрос по всем документам
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Взаиморасчеты.Сделка КАК ДокументСсылка,
        |   ИСТИНА КАК Оплачено
        |ИЗ
        |   РегистрНакопления.ВзаиморасчетыСКонтрагентами КАК Взаиморасчеты
        |ГДЕ
        |   Взаиморасчеты.Сделка В(&МассивДокументов)
        |   И Взаиморасчеты.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход)
        |
        |СГРУППИРОВАТЬ ПО
        |   Взаиморасчеты.Сделка";
        
    Запрос.УстановитьПараметр("МассивДокументов", МассивДокументов);
    РезультатЗапроса = Запрос.Выполнить();
    
    // Шаг 3: Заполняем Соответствие для быстрого поиска в памяти
    СоответствиеОплат = Новый Соответствие;
    Выборка = РезультатЗапроса.Выбрать();
    Пока Выборка.Следующий() Цикл
        СоответствиеОплат.Вставить(Выборка.ДокументСсылка, Выборка.Оплачено);
    КонецЦикла;
    
    // Шаг 4: Быстро распределяем данные по ячейкам оформления строк
    Для Каждого ОформлениеСтроки Из ОформленияСтрок Цикл
        Если ОформлениеСтроки.ДанныеСтроки = Неопределено Тогда
            Продолжить;
        КонецЕсли;
        
        ТекущийДокумент = ОформлениеСтроки.ДанныеСтроки.Ссылка;
        СтатусОплаты = СоответствиеОплат[ТекущийДокумент];
        
        // Если в соответствии нет значения, значит оплаты не было (Ложь)
        Если СтатусОплаты = Неопределено Тогда
            СтатусОплаты = Ложь;
        КонецЕсли;
        
        // Выводим значение в виде флажка (галочки)
        ОформлениеСтроки.Ячейки.Оплачено.ОтображатьФлажок = Истина;
        ОформлениеСтроки.Ячейки.Оплачено.Флажок = СтатусОплаты;
    КонецЦикла;

КонецПроцедуры

Данный код работает в разы быстрее, чем запросы в цикле. Но у него остается одна скрытая проблема. Платформа 1С очень часто вызывает событие ПриПолученииДанных — например, при каждом движении мыши, активации строки или даже при минимальном скроллинге. Это означает, что даже при прокрутке списка вверх-вниз по уже просмотренным строкам база данных будет постоянно нагружаться однотипными запросами, создавая микроскопические, но заметные глазу задержки.

Решение 2. Кэширование данных на уровне формы (Избавляемся от фризов при скроллинге)

Для того чтобы сделать интерфейс максимально плавным, применим технику кэширования данных в памяти сеанса формы. Мы будем сохранять результаты запросов в переменную модуля формы. При повторном выводе строк система не будет обращаться к СУБД, а мгновенно возьмет данные из кэша.

Проанализируем, как изменится код нашей формы:


// В самом верху модуля формы объявляем переменную кэша
Перем мКэшОплаты;

Процедура ПриОткрытии()
    // Инициализируем кэш при открытии формы
    мКэшОплаты = Новый Соответствие;
КонецПроцедуры

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

КонецПроцедуры

Благодаря кэшированию при прокрутке списка назад или повторной отрисовке тех же строк обращений к БД происходить вообще не будет. Интерфейс станет плавным и отзывчивым.

Решение 3. Архитектурно верный подход через Регистр Сведений (Решаем проблему закрытых периодов)

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

В чем опасность хранения признака в реквизитах документа?
Представьте ситуацию: реализация товаров была проведена в марте. В конце квартала бухгалтерия сдала отчетность и полностью закрыла период для редактирования. В мае покупатель присылает оплату. Если статус оплаты хранится внутри документа реализации, система попытается перезаписать документ реализации, чтобы установить признак Оплачено = Истина. Программа заблокирует эту транзакцию из-за закрытого периода. Открывать период ради изменения флага оплаты категорически запрещено методологией учета.

Решение — Независимый регистр сведений
Правильным архитектурным решением является создание нового независимого (или подчиненного регистратору) регистра сведений — например, СтатусыОплатыДокументов.

Запись в данный регистр выполняется программно из обработок проведения платежных документов (таких как ПлатежноеПоручениеВходящее, ПриходныйКассовыйОрдер). Поскольку регистр независимый, запись в него новой информации не затрагивает сами документы реализации и не требует открытия закрытого периода.

При таком подходе запрос в событии ПриПолученииДанных становится предельно простым и быстрым, так как выборка идет по индексу из плоской таблицы сведений:


// Пример простого и быстрого запроса к регистру сведений
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Статусы.Документ КАК ДокументСсылка,
    |   Статусы.Оплачено КАК Оплачено
    |ИЗ
    |   РегистрСведений.СтатусыОплатыДокументов КАК Статусы
    |ГДЕ
    |   Статусы.Документ В(&МассивНовыхДокументов)";

Решение 4. Использование свойства «ИспользоватьВЗапросе» для программных колонок

Малоизвестная оптимизационная возможность платформы 1С для работы с обычными формами заключается в правильной настройке колонок списка на уровне метаданных или программно.

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


// Пример программной настройки колонки при инициализации формы
НоваяКолонка = ЭлементыФормы.ДокументСписок.Колонки.Вставить(2, "Оплачено");
НоваяКолонка.Данные = "Оплачено";
НоваяКолонка.ИспользоватьВЗапросе = Истина; // Магия оптимизации платформы

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

Решение 5. Переход на управляемые формы (Динамический список)

Если ваша конфигурация находится в процессе модернизации, самым надежным и современным решением будет отказ от обычных форм списка в пользу управляемых (даже если вся остальная конфигурация работает в обычном режиме).

В управляемом интерфейсе элемент управления Таблица связывается с источником данных ДинамическийСписок. Динамический список работает на базе произвольного запроса, в котором вы можете выполнить полноценное ЛЕВОЕ СОЕДИНЕНИЕ основной таблицы документов с таблицами взаиморасчетов или регистром оплат:


// Пример запроса динамического списка в управляемой форме
ВЫБРАТЬ
    ДокументРеализация.Ссылка КАК Ссылка,
    ДокументРеализация.Номер КАК Номер,
    ДокументРеализация.Дата КАК Дата,
    ВЫБОР 
        КОГДА Оплаты.Оплачено ЕСТЬ НЕ NULL ТОГДА ИСТИНА 
        ИНАЧЕ ЛОЖЬ 
    КОНЕЦ КАК Оплачено
ИЗ
    Документ.РеализацияТоваровУслуг КАК ДокументРеализация
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СтатусыОплатыДокументов КАК Оплаты
        ПО ДокументРеализация.Ссылка = Оплаты.Документ

Платформа 1С на уровне технологической платформы сама оптимизирует этот запрос, накладывает необходимые отборы, сортировки и обеспечивает постраничную подгрузку данных (порциями по 30-50 строк) без единой строчки кода в обработчиках событий формы.

Сравнительный анализ решений

Подведем итог и систематизируем рассмотренные решения, чтобы вам было проще сделать выбор под конкретные требования вашей системы:

  1. Пакетный запрос в ПриПолученииДанных (Решение 1): Отличное решение для быстрого старта, не требует изменения структуры метаданных. Небольшие микро-задержки при быстром скроллинге больших списков.
  2. Пакетный запрос + Кэширование (Решение 2): Идеальный баланс производительности и трудозатрат для обычных форм. Полное отсутствие зависаний при скроллинге.
  3. Регистр сведений (Решение 3): Наиболее надежная архитектура корпоративного класса. Снимает нагрузку с расчетов «на лету», полностью решает проблему закрытых периодов. Требует создания нового объекта метаданных и доработки проведения документов оплаты.
  4. Использование свойства ИспользоватьВЗапросе (Решение 4): Самый простой код, но применим только при хранении признака внутри реквизитов самого документа.
  5. Динамический список управляемых форм (Решение 5): Современный стандарт разработки 1С. Полная автоматизация оптимизации со стороны СУБД, легкая поддержка, но требует перевода формы списка на управляемые технологии.

Для существующих баз на обычных формах (например, Управление торговлей 10.3 или Управление производственным предприятием 1.3) наиболее рекомендуемым сценарием является связка Решения №2 (кэширование) и Решения №3 (выделенный регистр сведений).

← На главную