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