Как проверить вхождение склада в доступные склады пользователя с учетом иерархии?

Программист 1С v8.3 (Обычные формы) Управленческий учет IT и автоматизация бизнеса
← На главную

В процессе разработки ограничений прав доступа или специализированных проверок в 1С:Предприятие мы часто сталкиваемся с необходимостью валидации данных. Типичный сценарий из практики: в конфигурации "Управление производственным предприятием 1.3" (УПП) необходимо в подписке на событие перед записью документа проверить, имеет ли текущий пользователь доступ к складу, указанному в документе.

Сложность задачи заключается в том, что в справочнике Пользователи в табличной части ДоступныеСклады могут быть указаны как конкретные элементы (склады), так и целые группы (папки складов). Нам необходимо корректно определить, входит ли склад документа в разрешенные группы или совпадает с одним из разрешенных элементов. В этой статье мы подробно рассмотрим возникшие проблемы при написании такого запроса, проанализируем ошибки и разберем три эффективных способа решения этой задачи с точки зрения производительности системы.

Почему не работает простое условие в запросе?

Начинающие разработчики часто пытаются написать интуитивное условие в запросе, которое выглядит следующим образом:

&СкладДокумента В ИЕРАРХИИ(ПользователиДоступныеСклады.Склад)

При попытке сохранить или выполнить такой запрос платформа 1С выдаст ошибку: "Неверные параметры В ИЕРАРХИИ". Проанализируем причину этой ошибки. Оператор В ИЕРАРХИИ на уровне СУБД транслируется в сложную рекурсивную выборку. Платформа 1С требует, чтобы в качестве параметра внутри оператора В ИЕРАРХИИ передавалось конкретное значение (ссылка), массив, список значений или статический подзапрос. Использовать в качестве аргумента динамическое поле из выбираемой таблицы (в данном случае поле нашей табличной части) синтаксис языка запросов 1С не позволяет.

Выяснив причину ошибки, перейдем к рассмотрению рабочих и оптимальных вариантов реализации данной проверки.

Способ 1. Получение массива и проверка через запрос

Это классический и самый простой для понимания способ. Сначала мы программно выгружаем все доступные пользователю склады во временный массив, а затем передаем этот массив в качестве параметра в запрос. Рассмотрим пример кода:


// Получаем ссылку на текущего пользователя
ТекущийПользователь = ПараметрыСеанса.ТекущийПользователь;

// Выгружаем колонку со складами в массив
МассивДоступныхСкладов = ТекущийПользователь.ДоступныеСклады.ВыгрузитьКолонку("Склад");

// Проверяем, есть ли вообще заполненные склады
Если МассивДоступныхСкладов.Количество() = 0 Тогда
    Предупреждение("У вас нет прав на работу со складами!");
    Отказ = Истина;
    Возврат;
КонецЕсли;

Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ ПЕРВЫЕ 1
|	1 КАК ДоступРазрешен
|ИЗ
|	Справочник.Склады КАК Склады
|ГДЕ
|	Склады.Ссылка = &СкладДокумента
|	И Склады.Ссылка В ИЕРАРХИИ(&МассивДоступныхСкладов)";

Запрос.УстановитьПараметр("МассивДоступныхСкладов", МассивДоступныхСкладов);
Запрос.УстановитьПараметр("СкладДокумента", Источник.Склад);

РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
    // Склад документа не входит в разрешенный список
    ДоступРазрешен = Ложь;
Иначе
    ДоступРазрешен = Истина;
КонецЕсли;

Важное замечание по оптимизации: Обратите внимание, как мы получаем текущего пользователя. В коде из обсуждения на форуме использовался поиск по коду через НайтиПоКоду(ПользователиИнформационнойБазы.ТекущийПользователь()). Это крайне неоптимально, так как вызывает дополнительное обращение к СУБД и замедляет работу подписки. Мы заменили этот вызов на мгновенное получение значения из кэша через ПараметрыСеанса.ТекущийПользователь.

Способ 2. Оптимальный пакетный запрос с временной таблицей (Best Practice для SQL)

Если нам необходимо выполнить всю операцию строго в рамках одного запроса к базе данных без промежуточной выгрузки в код (например, при сложной интеграции или жестких требованиях к архитектуре), мы должны использовать пакетный запрос с временной таблицей. Напрямую вставлять подзапрос внутрь оператора В ИЕРАРХИИ крайне не рекомендуется, так как оптимизатор СУБД (например, MS SQL Server) не сможет построить эффективный план выполнения, что приведет к сканированию всей таблицы и блокировкам.

Рассмотрим, как правильно составить пакетный запрос с индексацией временной таблицы:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|	ПользователиДоступныеСклады.Склад КАК Склад
|ПОМЕСТИТЬ ВТ_ДоступныеСклады
|ИЗ
|	Справочник.Пользователи.ДоступныеСклады КАК ПользователиДоступныеСклады
|ГДЕ
|	ПользователиДоступныеСклады.Ссылка = &ТекущийПользователь
|	И ПользователиДоступныеСклады.Склад <> ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ИЗМЕНЕНИЕ ИНДЕКСА ВТ_ДоступныеСклады ПО Склад;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ ПЕРВЫЕ 1
|	1 КАК ДоступРазрешен
|ИЗ
|	Справочник.Склады КАК Склады
|ГДЕ
|	Склады.Ссылка = &СкладДокумента
|	И Склады.Ссылка В ИЕРАРХИИ (ВЫБРАТЬ ВТ.Склад ИЗ ВТ_ДоступныеСклады КАК ВТ)";

Запрос.УстановитьПараметр("ТекущийПользователь", ПараметрыСеанса.ТекущийПользователь);
Запрос.УстановитьПараметр("СкладДокумента", Источник.Склад);

РезультатЗапроса = Запрос.Выполнить();
ДоступРазрешен = НЕ РезультатЗапроса.Пустой();

Ловушка пустой ссылки: Обратите особое внимание на строку условия ПользователиДоступныеСклады.Склад <> ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка). Если пользователь случайно добавит пустую строку в свою табличную часть или в базе останутся битые ссылки, оператор В ИЕРАРХИИ по пустой ссылке заставит СУБД рекурсивно обходить весь справочник от корня. На крупных базах данных это мгновенно вызывает зависание сеанса и загрузку процессора сервера на 100%. Мы застраховались от этой проблемы, принудительно отсекая пустые значения на этапе формирования временной таблицы.

Способ 3. Программный обход дерева (без обращений к СУБД)

Поскольку проверка выполняется внутри подписки на событие при записи документа, критически важно сделать её максимально быстрой. База данных под нагрузкой может быть занята транзакциями, и любой лишний SQL-запрос снижает общую пропускную способность системы.

Справочник Склады в реальных базах данных редко содержит более нескольких десятков или сотен элементов, а глубина вложенности папок обычно не превышает 3–4 уровней. В таких условиях наиболее производительным решением будет программный обход дерева родителей склада "вверх" непосредственно в коде 1С, без отправки тяжелых запросов к СУБД.

Проанализируем пример такого алгоритма:


// Получаем массив разрешенных складов из сеанса
МассивДоступныхСкладов = ПараметрыСеанса.ТекущийПользователь.ДоступныеСклады.ВыгрузитьКолонку("Склад");

// Удаляем пустые ссылки из массива для предотвращения ложных срабатываний
Пока Истина Цикл
	ИндексПустой = МассивДоступныхСкладов.Найти(Справочники.Склады.ПустаяСсылка());
	Если ИндексПустой = Неопределено Тогда 
		Прервать; 
	КонецЕсли;
	МассивДоступныхСкладов.Удалить(ИндексПустой);
КонецЦикла;

ДоступРазрешен = Ложь;
ТекСклад = Источник.Склад;

// Обходим дерево родителей склада вверх до самого корня
Пока ЗначениеЗаполнено(ТекСклад) Цикл
	// Если текущий элемент или его родитель найден в списке разрешенных - доступ открыт
	Если МассивДоступныхСкладов.Найти(ТекСклад) <> Неопределено Тогда
		ДоступРазрешен = Истина;
		Прервать;
	КонецЕсли;
	
	// Переходим к родителю. Платформа берет данные из объектного кэша мгновенно
	ТекСклад = ТекСклад.Родитель; 
КонецЦикла;

Если НЕ ДоступРазрешен Тогда
	ВызватьИсключение "У вас нет прав на проведение документов по складу: " + Источник.Склад;
КонецЕсли;

В чем преимущество данного метода? Платформа 1С:Предприятие кэширует считанные объекты справочников в памяти сервера приложений. При обращении к свойству Родитель платформа не делает каждый раз полноценный запрос к СУБД, а мгновенно извлекает данные из локального кэша сеанса. Данный алгоритм выполняется за доли миллисекунды и гарантирует полное отсутствие дополнительных транзакционных блокировок базы данных.

Подведем итоги

Мы рассмотрели три качественных способа решения задачи проверки прав доступа с учетом иерархии групп складов. Резюмируем правила выбора лучшего решения:

  1. Используйте Способ 3 (Программный обход) в подписках на события и высоконагруженных транзакциях записи документов. Это самый быстрый и безопасный способ для небольших иерархических справочников.
  2. Используйте Способ 2 (Пакетный запрос с индексацией), если проверка является частью сложного отчета или пакетной обработки данных, где невозможно обойтись без использования СУБД.
  3. Всегда фильтруйте пустые ссылки перед использованием оператора В ИЕРАРХИИ, чтобы избежать критической нагрузки на сервер.
← На главную