В процессе разработки ограничений прав доступа или специализированных проверок в 1С:Предприятие мы часто сталкиваемся с необходимостью валидации данных. Типичный сценарий из практики: в конфигурации "Управление производственным предприятием 1.3" (УПП) необходимо в подписке на событие перед записью документа проверить, имеет ли текущий пользователь доступ к складу, указанному в документе.
Сложность задачи заключается в том, что в справочнике Пользователи в табличной части ДоступныеСклады могут быть указаны как конкретные элементы (склады), так и целые группы (папки складов). Нам необходимо корректно определить, входит ли склад документа в разрешенные группы или совпадает с одним из разрешенных элементов. В этой статье мы подробно рассмотрим возникшие проблемы при написании такого запроса, проанализируем ошибки и разберем три эффективных способа решения этой задачи с точки зрения производительности системы.
Начинающие разработчики часто пытаются написать интуитивное условие в запросе, которое выглядит следующим образом:
&СкладДокумента В ИЕРАРХИИ(ПользователиДоступныеСклады.Склад)
При попытке сохранить или выполнить такой запрос платформа 1С выдаст ошибку: "Неверные параметры В ИЕРАРХИИ". Проанализируем причину этой ошибки. Оператор В ИЕРАРХИИ на уровне СУБД транслируется в сложную рекурсивную выборку. Платформа 1С требует, чтобы в качестве параметра внутри оператора В ИЕРАРХИИ передавалось конкретное значение (ссылка), массив, список значений или статический подзапрос. Использовать в качестве аргумента динамическое поле из выбираемой таблицы (в данном случае поле нашей табличной части) синтаксис языка запросов 1С не позволяет.
Выяснив причину ошибки, перейдем к рассмотрению рабочих и оптимальных вариантов реализации данной проверки.
Это классический и самый простой для понимания способ. Сначала мы программно выгружаем все доступные пользователю склады во временный массив, а затем передаем этот массив в качестве параметра в запрос. Рассмотрим пример кода:
// Получаем ссылку на текущего пользователя
ТекущийПользователь = ПараметрыСеанса.ТекущийПользователь;
// Выгружаем колонку со складами в массив
МассивДоступныхСкладов = ТекущийПользователь.ДоступныеСклады.ВыгрузитьКолонку("Склад");
// Проверяем, есть ли вообще заполненные склады
Если МассивДоступныхСкладов.Количество() = 0 Тогда
Предупреждение("У вас нет прав на работу со складами!");
Отказ = Истина;
Возврат;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| 1 КАК ДоступРазрешен
|ИЗ
| Справочник.Склады КАК Склады
|ГДЕ
| Склады.Ссылка = &СкладДокумента
| И Склады.Ссылка В ИЕРАРХИИ(&МассивДоступныхСкладов)";
Запрос.УстановитьПараметр("МассивДоступныхСкладов", МассивДоступныхСкладов);
Запрос.УстановитьПараметр("СкладДокумента", Источник.Склад);
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
// Склад документа не входит в разрешенный список
ДоступРазрешен = Ложь;
Иначе
ДоступРазрешен = Истина;
КонецЕсли;
Важное замечание по оптимизации: Обратите внимание, как мы получаем текущего пользователя. В коде из обсуждения на форуме использовался поиск по коду через НайтиПоКоду(ПользователиИнформационнойБазы.ТекущийПользователь()). Это крайне неоптимально, так как вызывает дополнительное обращение к СУБД и замедляет работу подписки. Мы заменили этот вызов на мгновенное получение значения из кэша через ПараметрыСеанса.ТекущийПользователь.
Если нам необходимо выполнить всю операцию строго в рамках одного запроса к базе данных без промежуточной выгрузки в код (например, при сложной интеграции или жестких требованиях к архитектуре), мы должны использовать пакетный запрос с временной таблицей. Напрямую вставлять подзапрос внутрь оператора В ИЕРАРХИИ крайне не рекомендуется, так как оптимизатор СУБД (например, MS SQL Server) не сможет построить эффективный план выполнения, что приведет к сканированию всей таблицы и блокировкам.
Рассмотрим, как правильно составить пакетный запрос с индексацией временной таблицы:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПользователиДоступныеСклады.Склад КАК Склад
|ПОМЕСТИТЬ ВТ_ДоступныеСклады
|ИЗ
| Справочник.Пользователи.ДоступныеСклады КАК ПользователиДоступныеСклады
|ГДЕ
| ПользователиДоступныеСклады.Ссылка = &ТекущийПользователь
| И ПользователиДоступныеСклады.Склад <> ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ИЗМЕНЕНИЕ ИНДЕКСА ВТ_ДоступныеСклады ПО Склад;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ ПЕРВЫЕ 1
| 1 КАК ДоступРазрешен
|ИЗ
| Справочник.Склады КАК Склады
|ГДЕ
| Склады.Ссылка = &СкладДокумента
| И Склады.Ссылка В ИЕРАРХИИ (ВЫБРАТЬ ВТ.Склад ИЗ ВТ_ДоступныеСклады КАК ВТ)";
Запрос.УстановитьПараметр("ТекущийПользователь", ПараметрыСеанса.ТекущийПользователь);
Запрос.УстановитьПараметр("СкладДокумента", Источник.Склад);
РезультатЗапроса = Запрос.Выполнить();
ДоступРазрешен = НЕ РезультатЗапроса.Пустой();
Ловушка пустой ссылки: Обратите особое внимание на строку условия ПользователиДоступныеСклады.Склад <> ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка). Если пользователь случайно добавит пустую строку в свою табличную часть или в базе останутся битые ссылки, оператор В ИЕРАРХИИ по пустой ссылке заставит СУБД рекурсивно обходить весь справочник от корня. На крупных базах данных это мгновенно вызывает зависание сеанса и загрузку процессора сервера на 100%. Мы застраховались от этой проблемы, принудительно отсекая пустые значения на этапе формирования временной таблицы.
Поскольку проверка выполняется внутри подписки на событие при записи документа, критически важно сделать её максимально быстрой. База данных под нагрузкой может быть занята транзакциями, и любой лишний SQL-запрос снижает общую пропускную способность системы.
Справочник Склады в реальных базах данных редко содержит более нескольких десятков или сотен элементов, а глубина вложенности папок обычно не превышает 3–4 уровней. В таких условиях наиболее производительным решением будет программный обход дерева родителей склада "вверх" непосредственно в коде 1С, без отправки тяжелых запросов к СУБД.
Проанализируем пример такого алгоритма:
// Получаем массив разрешенных складов из сеанса
МассивДоступныхСкладов = ПараметрыСеанса.ТекущийПользователь.ДоступныеСклады.ВыгрузитьКолонку("Склад");
// Удаляем пустые ссылки из массива для предотвращения ложных срабатываний
Пока Истина Цикл
ИндексПустой = МассивДоступныхСкладов.Найти(Справочники.Склады.ПустаяСсылка());
Если ИндексПустой = Неопределено Тогда
Прервать;
КонецЕсли;
МассивДоступныхСкладов.Удалить(ИндексПустой);
КонецЦикла;
ДоступРазрешен = Ложь;
ТекСклад = Источник.Склад;
// Обходим дерево родителей склада вверх до самого корня
Пока ЗначениеЗаполнено(ТекСклад) Цикл
// Если текущий элемент или его родитель найден в списке разрешенных - доступ открыт
Если МассивДоступныхСкладов.Найти(ТекСклад) <> Неопределено Тогда
ДоступРазрешен = Истина;
Прервать;
КонецЕсли;
// Переходим к родителю. Платформа берет данные из объектного кэша мгновенно
ТекСклад = ТекСклад.Родитель;
КонецЦикла;
Если НЕ ДоступРазрешен Тогда
ВызватьИсключение "У вас нет прав на проведение документов по складу: " + Источник.Склад;
КонецЕсли;
В чем преимущество данного метода? Платформа 1С:Предприятие кэширует считанные объекты справочников в памяти сервера приложений. При обращении к свойству Родитель платформа не делает каждый раз полноценный запрос к СУБД, а мгновенно извлекает данные из локального кэша сеанса. Данный алгоритм выполняется за доли миллисекунды и гарантирует полное отсутствие дополнительных транзакционных блокировок базы данных.
Мы рассмотрели три качественных способа решения задачи проверки прав доступа с учетом иерархии групп складов. Резюмируем правила выбора лучшего решения:
В ИЕРАРХИИ, чтобы избежать критической нагрузки на сервер.