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