При проектировании сложных интерфейсов в системе «1С:Предприятие 8» разработчики часто сталкиваются с задачей сбора данных из нескольких источников (справочников, документов или регистров) и их последующего отображения в виде таблиц на управляемой форме. На первый взгляд задача кажется тривиальной: написать запрос, выгрузить результат и передать его на форму. Однако здесь кроется множество подводных камней, начиная от катастрофического падения производительности СУБД из-за неоптимальных запросов и заканчивая ошибками передачи типов между клиентом и сервером.
В рамках этой статьи мы подробно разберем, как правильно спроектировать этот процесс. Мы выясним причину возникновения одной из самых опасных ошибок в запросах — декартова произведения, научимся работать с временными таблицами и пакетными запросами, детально изучим механизм взаимодействия между серверным объектом ТаблицаЗначений и интерфейсным объектом ДанныеФормыКоллекция, а также рассмотрим стандарты разработки фирмы «1С» в отношении именования реквизитов.
Давайте по шагам разберем каждый этап решения этой задачи.
Проанализируем запрос, который изначально пытался использовать автор темы:
ВЫБРАТЬ
ВыборЭлектродвигателя.hбуф КАК hбуф,
ГлубинаПогруженияЭЦНПодДинамическийУровень.aн КАК aн,
ВыборКабеляТрансформатораИОпределениеЭксплуатационныхПараметровЭЦН.dк КАК dк
ИЗ
Справочник.ВыборЭлектродвигателя КАК ВыборЭлектродвигателя,
Справочник.ГлубинаПогруженияЭЦНПодДинамическийУровень КАК ГлубинаПогруженияЭЦНПодДинамическийУровень,
Справочник.ВыборКабеляТрансформатораИОпределениеЭксплуатационныхПараметровЭЦН КАК ВыборКабеляТрансформатораИОпределениеЭксплуатационныхПараметровЭЦН
Обратите внимание на секцию ИЗ. Здесь через запятую перечислены три таблицы справочников. При этом в запросе полностью отсутствуют условия соединения (секция ПО) или условия фильтрации (секция ГДЕ). С точки зрения теории реляционных баз данных и SQL, такая конструкция порождает декартово произведение (произведение множеств или Cross Join).
Рассмотрим подробнее, к чему это приводит на практике. Допустим, в каждом из трех справочников содержится всего по 100 записей. При выполнении такого запроса СУБД попытается сопоставить каждую строку первой таблицы с каждой строкой второй, а затем полученный результат — с каждой строкой третьей таблицы. Итоговое количество строк в результате составит:
100 * 100 * 100 = 1 000 000 строк!
Если в реальной базе данных количество элементов в справочниках будет исчисляться тысячами, сервер СУБД либо зависнет из-за нехватки оперативной памяти при построении этой гигантской виртуальной «каши», либо завершит работу по таймауту. При этом реальной смысловой нагрузки эти дублирующиеся строки не несут.
Выясним причину, почему автору потребовался такой запрос. Ему необходимо заполнить две независимые таблицы на одной форме (например, Таблица 1 и Таблица 2). Данные для этих таблиц выбираются из разных справочников. Следовательно, соединять эти справочники в один запрос нет никакого физического смысла.
Для решения этой задачи мы применим пакетный запрос и механизм временных таблиц. Это позволит нам за одно обращение к серверу БД получить данные для обеих таблиц без создания паразитных связей. Рассмотрим пример правильного построения пакетного запроса на сервере:
&НаСервере
Функция ПолучитьДанныеДляТаблицНаСервере() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Двигатели.ВысотаБуфера КАК ВысотаБуфера,
| Двигатели.ЧастотаВращения КАК ЧастотаВращения,
| Двигатели.КоэффициентПолезногоДействия КАК КПД
|ПОМЕСТИТЬ ВТ_Двигатели
|ИЗ
| Справочник.ВыборЭлектродвигателя КАК Двигатели;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Глубина.Коэффициент КАК Коэффициент,
| Глубина.ДавлениеНагнетания КАК ДавлениеНагнетания,
| Глубина.ТемператураПласта КАК ТемператураПласта
|ПОМЕСТИТЬ ВТ_ГлубинаПогружения
|ИЗ
| Справочник.ГлубинаПогруженияЭЦНПодДинамическийУровень КАК Глубина;
|
|////////////////////////////////////////////////////////////////////////////////
|// Запрос 1: Получаем данные для Таблицы 1
|ВЫБРАТЬ
| ВТ_Двигатели.ВысотаБуфера КАК ВысотаБуфера,
| ВТ_Двигатели.ЧастотаВращения КАК ЧастотаВращения,
| ВТ_Двигатели.КПД КАК КПД
|ИЗ
| ВТ_Двигатели КАК ВТ_Двигатели;
|
|////////////////////////////////////////////////////////////////////////////////
|// Запрос 2: Получаем данные для Таблицы 2
|ВЫБРАТЬ
| ВТ_ГлубинаПогружения.Коэффициент КАК Коэффициент,
| ВТ_ГлубинаПогружения.ДавлениеНагнетания КАК ДавлениеНагнетания,
| ВТ_ГлубинаПогружения.ТемператураПласта КАК ТемператураПласта
|ИЗ
| ВТ_ГлубинаПогружения КАК ВТ_ГлубинаПогружения";
// Выполняем пакет запросов
РезультатыПакета = Запрос.ВыполнитьПакет();
// РезультатыПакета[0] и РезультатыПакета[1] - это создание временных таблиц (они пустые)
// РезультатыПакета[2] - результат первого итогового запроса (для Таблицы 1)
// РезультатыПакета[3] - результат второго итогового запроса (для Таблицы 2)
СтруктураРезультатов = Новый Структура;
СтруктураРезультатов.Вставить("ТаблицаДвигателей", РезультатыПакета[2].Выгрузить());
СтруктураРезультатов.Вставить("ТаблицаГлубины", РезультатыПакета[3].Выгрузить());
Возврат СтруктураРезультатов;
КонецФункции
Использование пакетного запроса исключает декартово произведение, так как выборка данных по каждому справочнику происходит в изолированных шагах пакета, а временные таблицы позволяют оптимизировать работу СУБД с памятью.
После того как мы получили данные на сервере в виде объектов типа ТаблицаЗначений, нам необходимо отобразить их на форме. Начинающие разработчики часто пытаются выполнить прямое присвоение на клиенте:
Объект.Таблица1 = ТаблицаЗначенийИзЗапроса;
Такой код неминуемо вызовет ошибку выполнения. Разберем по шагам, почему это происходит.
В управляемом приложении 1С действует строгое разделение контекста выполнения программного кода на Клиент и Сервер. Объект ТаблицаЗначений является исключительно серверным типом данных. Он не может существовать на стороне тонкого или веб-клиента. Он не сериализуется и не может быть передан «в чистом виде» с сервера на клиент.
Для отображения табличных данных на управляемой форме используется специальный универсальный тип-посредник — ДанныеФормыКоллекция. Этот тип данных умеет «жить» как на сервере, так и на клиенте, обеспечивая автоматическую передачу и отображение строк в табличном поле формы.
Платформа «1С:Предприятие 8» предоставляет эффективные механизмы для взаимного преобразования этих типов. Рассмотрим два основных способа переноса данных из серверной ТаблицыЗначений в реквизит формы типа ДанныеФормыКоллекция.
Этот способ является наиболее простым и предпочтительным, когда структура колонок реквизита управляемой формы и структура колонок полученной таблицы значений из запроса полностью совпадают. Обратите внимание, что имена колонок в запросе (псевдонимы КАК ...) должны в точности соответствовать именам реквизитов таблицы на форме.
&НаСервере
Процедура ЗаполнитьТаблицыНаФорме()
// Получаем структуру с таблицами значений из нашего пакетного запроса
РезультатыВыборки = ПолучитьДанныеДляТаблицНаСервере();
// Напрямую загружаем данные в реквизиты формы.
// "ТаблицаДвигателейНаФорме" и "ТаблицаГлубиныНаФорме" - это реквизиты формы с типом ДанныеФормыКоллекция
ЭтотОбъект.ТаблицаДвигателейНаФорме.Загрузить(РезультатыВыборки.ТаблицаДвигателей);
ЭтотОбъект.ТаблицаГлубиныНаФорме.Загрузить(РезультатыВыборки.ТаблицаГлубины);
КонецПроцедуры
Если вам требуется выполнить сложные манипуляции с типами данных или программно преобразовать всю структуру, можно использовать платформенные методы ЗначениеВРеквизитФормы() и РеквизитФормыВЗначение(). Эти методы доступны только в серверном контексте модуля формы.
&НаСервере
Процедура ЗаполнитьЧерезПреобразованиеКонтекста()
ДанныеИзЗапроса = ПолучитьДанныеДляТаблицНаСервере();
// Преобразуем ТаблицуЗначений в реквизит управляемой формы
ЗначениеВРеквизитФормы(ДанныеИзЗапроса.ТаблицаДвигателей, "ТаблицаДвигателейНаФорме");
ЗначениеВРеквизитФормы(ДанныеИзЗапроса.ТаблицаГлубины, "ТаблицаГлубиныНаФорме");
КонецПроцедуры
Использование метода Загрузить() работает быстрее, так как не требует перестроения структуры метаданных формы, а лишь очищает коллекцию и заполняет её новыми строками.
В сообщении 7 автор темы задается вопросом: создавать ли колонки для таблицы вручную в конфигураторе или генерировать их программно, исключая ненужные реквизиты?
Рекомендация стандартов разработки: если состав колонок таблицы заранее известен и стабилен, их всегда нужно создавать визуально в конфигураторе. Это облегчает дальнейшую поддержку программного кода, экономит серверные вызовы при открытии формы и позволяет визуально настроить свойства отображения (формат чисел, маску, ширину, заголовки, доступность).
Однако бывают ситуации, когда состав колонок действительно динамический (например, зависит от внешних настроек или от того, какие именно свойства справочников заполнены в конкретной базе данных). Рассмотрим, как правильно реализовать программное создание колонок на форме.
Процесс программного добавления колонок состоит из двух обязательных шагов:
Проанализируем пример кода, реализующий этот алгоритм:
&НаСервере
Процедура ПрограммноСоздатьКолонкиНаФорме()
// Массив, в который мы поместим новые реквизиты (данные)
ДобавляемыеРеквизиты = Новый Массив;
// Зададим имя родительского реквизита на форме (наша таблица)
ИмяТаблицыРодителя = "ТаблицаДвигателейНаФорме";
// Опишем типы для новых колонок
ОписаниеТипаЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2));
ОписаниеТипаСтрока = Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(150));
// Добавляем описание реквизитов в массив
// Параметры: ИмяРеквизита, Тип, ПутьКРодителю, Заголовок
ДобавляемыеРеквизиты.Добавить(New РеквизитФормы("ВысотаБуфера", ОписаниеТипаЧисло, ИмяТаблицыРодителя, "Высота буфера, мм"));
ДобавляемыеРеквизиты.Добавить(New РеквизитФормы("МодельДвигателя", ОписаниеТипаСтрока, ИмяТаблицыРодителя, "Модель двигателя"));
// Шаг 1. Изменяем структуру реквизитов формы на сервере
ЭтотОбъект.ИзменитьРеквизиты(ДобавляемыеРеквизиты);
// Шаг 2. Создаем элементы интерфейса для отображения колонок
Для Каждого Реквизит Из ДобавляемыеРеквизиты Цикл
// Создаем уникальное имя элемента формы
ИмяЭлементаКолонки = ИмяТаблицыРодителя + "_" + Реквизит.Имя;
// Добавляем элемент "ПолеФормы" внутрь элемента-таблицы
НовоеПоле = Элементы.Добавить(ИмяЭлементаКолонки, Тип("ПолеФормы"), Элементы[ИмяТаблицыРодителя]);
НовоеПоле.Вид = ВидПоляФормы.ПолеВвода;
// Связываем визуальный элемент с созданным ранее путем к данным реквизита
НовоеПоле.ПутьКДанным = ИмяТаблицыРодителя + "." + Реквизит.Имя;
// Дополнительные настройки интерфейса
НовоеПоле.КнопкаОчистки = Истина;
КонецЦикла;
КонецПроцедуры
Важное примечание: метод ИзменитьРеквизиты() является крайне ресурсоемкой операцией для сервера «1С:Предприятие», так как при его вызове происходит полная перезагрузка контекста формы. По этой причине старайтесь группировать все добавляемые реквизиты в один массив и вызывать этот метод единожды.
В сообщениях 10–14 участники форума обращают внимание на неудачные наименования реквизитов в коде автора (hбуф, hг, aн, Be, dк). Использование буквенных сокращений из математических формул или физических расчетов в качестве имен полей базы данных (метаданных) и переменных является грубым нарушением стандартов разработки фирмы «1С».
Разберем основные правила именования объектов метаданных и реквизитов по стандартам 1С:
hст.
hбуф, pж, Pу, Dэд.
ВысотаБуфера, ПлотностьЖидкости, УстьевоеДавление, ДиаметрЭлектродвигателя.КоэффициентПолезногоДействия, ГлубинаСпускаКабеля.ВысотаБуфера).Соблюдение этих простых стандартов гарантирует высокую читаемость вашего кода, минимизирует количество ошибок при командной разработке и значительно упрощает процесс поддержки и масштабирования прикладного решения.