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