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