Работа с правами доступа и выборкой пользователей в конфигурациях на базе БСП (Библиотека Стандартных Подсистем), таких как "1С:Розница", "1С:Управление Торговлей" или "1С:ERP", часто вызывает сложности. Основная проблема заключается в том, что права назначаются не только напрямую пользователям, но и через иерархические ГруппыПользователей.
В этой статье мы подробно разберем, как оптимизировать запрос, который должен выбрать всех пользователей, обладающих определенным профилем доступа, исключить "запрещенных" и при этом учесть сложную вложенность групп. Мы проанализируем ошибки исходного подхода и придем к наиболее производительному решению.
Давайте рассмотрим ситуацию. У нас есть задача: получить список пользователей, которые входят в справочник ГруппыДоступа с определенным профилем.
Исходный запрос, с которым мы сталкиваемся, пытается решить задачу "в лоб":
ГруппыПользователей.Состав).НЕ В).Такой подход имеет два критических недостатка:
1. Проблема производительности: Использование множественных временных таблиц, оператора ОБЪЕДИНИТЬ и условия НЕ В (...) может создавать неоптимальный план выполнения запроса на уровне СУБД.
2. Логическая ошибка (Иерархия): Справочник ГруппыПользователей поддерживает иерархию элементов. Группа может входить в другую группу. Обращение к табличной части Состав вернет только непосредственных участников. Если пользователь "сидит" глубоко внутри вложенной группы, стандартный запрос его "потеряет".
Первое, на что стоит обратить внимание при оптимизации — это типы соединений. В исходном варианте часто используется ЛЕВОЕ СОЕДИНЕНИЕ там, где мы точно ожидаем наличие данных, или где нам нужно отсечь лишнее.
Если наша цель — оставить только те записи, которые есть в обеих таблицах (например, пересечение пользователей и их групп), следует использовать ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Это дает подсказку оптимизатору СУБД, что можно использовать более эффективные алгоритмы хеширования или вложенных циклов.
Пример упрощения выборки, если бы мы работали с плоской структурой:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ВТ_Пользователи.ПользовательГруппыПользователей КАК Пользователь,
ВТ_Пользователи.ГруппаПользователей КАК Группа
ИЗ
Справочник.ГруппыДоступа.Пользователи КАК ГруппыДоступаПользователи
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПользователиИзНужныхГрупп КАК ВТ_Пользователи
ПО ГруппыДоступаПользователи.Пользователь = ВТ_Пользователи.ГруппаПользователей
Также, если мы используем временные таблицы и нам не нужны значения NULL, нет смысла делать левое соединение, а потом фильтровать ЕСТЬNULL. Лучше сразу отсечь лишнее внутренним соединением.
Самый важный момент в оптимизации запросов к правам в типовых конфигурациях — это использование специальных регистров. Мы не должны вручную разворачивать состав групп пользователей через справочники.
В конфигурациях на БСП существует служебный регистр сведений СоставыГруппПользователей (в свежих версиях может называться СоставыГруппПользователейИВнешнихПользователей).
Этот регистр содержит уже "развернутую" (плоскую) структуру. Система сама обновляет его при записи групп пользователей. В нем для каждой группы перечислены все конечные пользователи, включая тех, кто входит через подгруппы любого уровня вложенности.
Давайте перепишем запрос, используя этот регистр. Это позволит отказаться от сложных конструкций с ОБЪЕДИНИТЬ и вложенных запросов к составу справочника.
ВЫБРАТЬ РАЗЛИЧНЫЕ
СоставыГрупп.Пользователь КАК Пользователь
ИЗ
Справочник.ГруппыДоступа.Пользователи КАК ГруппыДоступаТЧ
// Соединяемся с регистром, который хранит полный состав групп
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СоставыГруппПользователей КАК СоставыГрупп
ПО ГруппыДоступаТЧ.Пользователь = СоставыГрупп.ГруппаПользователей
ГДЕ
// Отбираем только нужные профили доступа
ГруппыДоступаТЧ.Ссылка.Профиль = &НужныйПрофиль
// Важно: в регистре есть флаг использования, его нужно учитывать
И СоставыГрупп.Используется = ИСТИНА
Такой запрос работает в разы быстрее и, главное, корректно обрабатывает вложенность групп.
В исходной задаче было условие: исключить пользователей, которые входят в определенные "запрещенные" группы доступа. Автор использовал конструкцию:
ГДЕ НЕ ГруппыДоступа.Ссылка В (&СписокЗапрещенных).
Оператор NOT IN (НЕ В) часто является "убийцей" производительности, так как может приводить к полному сканированию таблиц (Full Scan). В мире SQL существует классический паттерн оптимизации, называемый Anti-Join (Анти-соединение).
Суть метода:
ЛЕВОЕ СОЕДИНЕНИЕ с таблицей, которую хотим исключить.ГДЕ проверяем, что поле из присоединенной таблицы равно NULL.Давайте применим это к нашему запросу. Предположим, мы хотим выбрать пользователей профиля "Менеджер", но исключить тех, кто входит в группу "Стажеры".
ВЫБРАТЬ РАЗЛИЧНЫЕ
СоставыГрупп.Пользователь КАК Пользователь
ИЗ
Справочник.ГруппыДоступа.Пользователи КАК ГруппыДоступаТЧ
// Присоединяем состав групп (как рассматривали выше)
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СоставыГруппПользователей КАК СоставыГрупп
ПО ГруппыДоступаТЧ.Пользователь = СоставыГрупп.ГруппаПользователей
// АНТИ-СОЕДИНЕНИЕ: Пытаемся найти этого же пользователя в запрещенных группах
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ГруппыДоступа.Пользователи КАК ЗапрещенныеГруппы
ПО СоставыГрупп.Пользователь = ЗапрещенныеГруппы.Пользователь
И ЗапрещенныеГруппы.Ссылка В (&СписокЗапрещенныхГрупп)
ГДЕ
ГруппыДоступаТЧ.Ссылка.Профиль = &РазрешенныйПрофиль
И СоставыГрупп.Используется = ИСТИНА
// Оставляем только тех, для кого соединение с запрещенными группами НЕ удалось
И ЗапрещенныеГруппы.Ссылка ЕСТЬ NULL
Таким образом, СУБД использует индексы для соединения, а затем просто отфильтровывает пустые значения, что значительно эффективнее перебора НЕ В.
Иногда попытка решить задачу одним гигантским запросом — не лучший путь. Если логика проверки прав сложная ("если есть роль А, но нет роли Б, или если есть роль В..."), эффективнее вынести логику в функцию общего модуля. Вместо выборки всех пользователей, можно создать функцию проверки для конкретного текущего пользователя, если задача стоит в ограничении доступа "здесь и сейчас". Пример функции, которая проверяет наличие запрещающей роли (или группы) для текущего пользователя:
Функция ЕстьЗапретНаРедактирование(Пользователь)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| 1
|ИЗ
| Справочник.ГруппыДоступа.Пользователи КАК ГруппыДоступа
|ГДЕ
| ГруппыДоступа.Ссылка В (&СписокЗапрещающихГрупп)
| И ГруппыДоступа.Пользователь = &Пользователь";
Запрос.УстановитьПараметр("Пользователь", Пользователь);
Запрос.УстановитьПараметр("СписокЗапрещающихГрупп", ПолучитьСписокЗапрета());
// Если результат не пустой — значит, пользователь найден в "черном списке"
Результат = Запрос.Выполнить();
Возврат НЕ Результат.Пустой();
КонецФункции
Также стоит помнить, что в БСП есть готовые методы в модуле УправлениеДоступом. Например, функции для получения всех пользователей, входящих в группу доступа, уже учитывают иерархию и замещения. Использование стандартных методов API конфигурации всегда предпочтительнее "самописных" запросов, так как разработчики платформы уже решили проблемы оптимизации внутри этих методов.
Для создания максимально быстрого и надежного отчета или выборки:
1. Используйте РегистрСведений.СоставыГруппПользователей вместо ручного разбора справочника ГруппыПользователей.
2. Заменяйте условия НЕ В (...) на конструкцию ЛЕВОЕ СОЕДИНЕНИЕ ... ГДЕ ... ЕСТЬ NULL.
3. Избегайте ВЫБРАТЬ РАЗЛИЧНЫЕ во вложенных запросах, если можно сгруппировать данные на финальном этапе.
4. Если возможно, используйте прикладной интерфейс (API) подсистемы "Управление доступом".