Производительность и частые ошибки
Производительность запросов к структуре компании зависит от объема выборки. Ограничивайте структуру, типы узлов, глубину обхода, активность, права и размер страницы до выполнения запроса. После выполнения проверяйте результат по контракту метода. Одиночный объект, коллекция, массив и значения true и false по-разному обозначают отсутствие данных.
Выбрать способ оптимизации
Начинайте с ограничения области запроса. Кеширование и сокращение набора полей применяйте после того, как запрос выбирает только необходимые узлы или связи.
|
Задача |
Решение |
Как проверить |
|
Получить один известный узел или типовую коллекцию |
Использовать готовый метод сервиса |
Сравнить тип и объем результата с контрактом метода |
|
Объединить условия по иерархии, активности, ролям и доступу |
Использовать билдер с составным фильтром |
Проверить каждый фильтр отдельно, затем итоговую коллекцию |
|
Прочитать большую коллекцию |
Задать |
Убедиться, что страницы не пропускают и не повторяют элементы |
|
Обойти дерево |
Выбрать минимальную достаточную глубину |
Сравнить полученные уровни с исходным узлом и направлением обхода |
|
Получить часть свойств объекта |
Задать |
Обратиться ко всем свойствам, которые использует вызывающий код |
|
Повторять одинаковый запрос |
Выбрать допустимое время жизни кеша |
Проверить данные сразу после изменения структуры |
|
Обработать несколько идентификаторов |
Использовать метод, который принимает массив |
Сопоставить ключи или идентификаторы результата со входным массивом |
|
Дополнить запрос пользователей сведениями о структуре |
Ограничить подзапрос типами и идентификаторами узлов |
Проверить число строк и уникальных пользователей |
Готовый сервис выбирайте для типовой операции. Билдер используйте, когда один запрос должен объединить несколько фильтров. Критерии выбора смотрите в статье Архитектура модуля и выбор сервиса.
Ограничить объем выборки
Сначала ограничьте объем результата фильтрами, глубиной обхода, пагинацией и набором полей. Затем настройте способ загрузки этих данных.
Передать структуру, тип и активность явно
Передавайте идентификатор структуры, типы узлов и фильтр активности явно, если значения по умолчанию не соответствуют задаче. Сводная таблица значений по умолчанию приведена в статье Архитектура модуля и выбор сервиса, а параметры методов — в статье Узлы структуры компании.
Не отключайте фильтры структуры, типа и активности одновременно для поиска одной записи. Широкий запрос увеличивает объем результата и затрудняет диагностику. Одинаковый идентификатор из другого сценария или неактивная связь может выглядеть как нужная запись.
Выбрать минимальную глубину обхода
Минимальная глубина уменьшает число узлов, которые сервис или билдер должен прочитать и преобразовать. Значения depthLevel описаны в разделе Задать глубину обхода, а режимы поиска подчиненных — в разделе Получить подчиненных.
Разбить коллекцию на страницы
Пагинация ограничивает память и время обработки одной страницы. Для стабильного результата сохраняйте одинаковые фильтры между страницами и задавайте однозначную сортировку. Ограничения сервисов и билдеров описаны в разделах Ограничить количество результатов и Отсортировать результат.
Ограничить набор полей билдера
Метод setSelect() полностью заменяет стандартный набор ORM-полей. Сохраняйте обязательные поля объекта результата и все свойства, которые использует дальнейший код. Состав полей приведен в разделе Ограничить выбираемые поля и кеш.
Сократить повторные запросы
Пакетные методы и кеш ORM решают разные задачи. Метод для массива идентификаторов формирует общий результат и упрощает обработку нескольких объектов, а кеш повторно использует данные одинаковой выборки. Пакетный контракт сам по себе не гарантирует один запрос к базе данных.
Использовать методы для нескольких объектов
Передавайте массив идентификаторов в пакетный метод, если такой метод предусмотрен для задачи. Пакетные методы могут пропускать часть входных идентификаторов или подставлять значения по умолчанию, поэтому сначала проверьте контракт результата. Форматы пакетных результатов описаны в разделах Получить руководителей нескольких пользователей и Учесть различия значений по умолчанию.
Настроить время жизни кеша
Выбирайте время жизни кеша по допустимому сроку устаревания структуры, участников и ролей. Если выборка не отражает недавнее изменение, повторите запрос без кеша. Значения и особенности билдеров описаны в разделе Ограничить выбираемые поля и кеш.
Не создавайте новый кеш поверх результата, пока не определили допустимый срок устаревания. Иначе несколько уровней кеширования затруднят поиск источника старых данных.
Загрузить один элемент через get()
Метод get() ограничивает выборку одним элементом, а условие Last выбирает элемент из уже загруженной коллекции. Выберите вариант до выполнения запроса и не используйте один экземпляр билдера для независимых вызовов get() и getAll(). Подробности приведены в разделе Выбрать первый или последний элемент коллекции.
Оптимизировать запрос пользователей
Ограничьте подзапрос пользователей нужными типами и идентификаторами узлов. Связи с несколькими узлами или ролями могут дублировать строки, поэтому примените setDistinct(), если нужен уникальный список пользователей. Порядок подготовки подзапроса и сортировки приведен в разделе Отсортировать пользователей по узлам и ролям.
Как выявить проблемы с результатом
Диагностируйте запрос от входных данных к результату. Такой порядок отделяет отсутствие данных от фильтрации, кеша, прав и ошибок слоя данных.
-
Проверьте подключение модуля
humanresources. -
Запишите входные идентификаторы, типы узлов, структуру и ожидаемый формат результата.
-
Проверьте, что идентификаторы больше нуля, существуют и относятся к нужному типу объектов.
-
Сравните явные параметры со значениями метода по умолчанию.
-
Временно проверьте каждый фильтр отдельно: структуру, тип, активность, глубину, роли и доступ.
-
Убедитесь, что
limit,offsetи сортировка не исключают или не переставляют нужную запись. -
Повторите чтение без кеша, если данные недавно изменились.
-
Сравните фактический тип результата с контрактом метода.
-
Зафиксируйте исключение и контекст запроса на уровне приложения, если метод передает ошибку вызывающему коду.
Различить пустые результаты
Пустой результат не всегда означает ошибку. Его смысл определяет контракт конкретного метода.
|
Результат |
Возможное значение |
Что проверить |
|
|
Одиночный объект не найден |
Идентификатор, структуру, тип, активность и доступ |
|
Пустая коллекция |
Совпадений нет либо метод преобразовал ошибку в пустой результат |
Фильтры, роли, права и описание конкретного метода |
|
Пустой массив |
Нет идентификаторов, групп или сохраненных настроек |
Формат ключей, значения по умолчанию и входной массив |
|
|
Проверяемое отношение или роль не подтверждены |
Тип узла, активные связи и роли пользователей |
|
|
Количество равно нулю либо исходная структура не найдена |
Контракт счетчика и структуру по умолчанию |
|
|
Значение для подразделения не сохраняли |
Не заменять на |
Методы getUserHeads() и getUserDeputies() класса Bitrix\HumanResources\Public\Service\Node\UserService перехватывают внутренние ошибки и возвращают пустую коллекцию. Если приложению нужно отличить ошибку от отсутствия руководителей, записывайте контекст запроса и связанные ошибки на уровне приложения.
Фильтр Bitrix\HumanResources\Builder\Structure\Filter\SelectionCondition\Node\NodeAccessFilter также может преобразовать ошибку проверки доступа в пустой результат билдера. При неожиданно пустой коллекции отдельно проверьте идентификатор пользователя, тип узла, действие и допустимые уровни разрешения.
Проверить типы узлов и глубину
Команды могут отсутствовать из-за значения Bitrix\HumanResources\Type\NodeEntityType::DEPARTMENT по умолчанию. Передайте значение Bitrix\HumanResources\Type\NodeEntityType::TEAM или оба типа явно. Если пропущены дальние родители или потомки, замените Bitrix\HumanResources\Enum\DepthLevel::FIRST на требуемое число уровней или Bitrix\HumanResources\Enum\DepthLevel::FULL.
Не расширяйте тип и глубину одновременно при диагностике. Сначала добавьте нужный тип узла, проверьте результат, затем измените глубину. Последовательная проверка покажет, какое условие исключало данные.
Проверить структуру и активность
Идентификатор структуры определяет область поиска. Значение null в одних API отключает ограничение, а в других выбирает структуру по умолчанию. Сверьте поведение конкретного метода перед тем, как убирать параметр.
Фильтры активности могут исключить неактивный узел, ветвь или связь участника. Не отключайте их постоянно ради получения результата. Сначала убедитесь, что вызывающий сценарий действительно должен видеть неактивные данные, затем передайте подходящее значение фильтра явно.
Проверить кеш и пагинацию
Старые данные сразу после изменения указывают на неподходящее время жизни кеша или внешний уровень кеширования. Повторите запрос без кеша билдера. Если результат обновился, выберите время жизни кеша по допустимому сроку устаревания данных.
Пропуск или повтор строк между страницами указывает на нестабильную сортировку либо изменение данных во время обхода. Задайте однозначный порядок и сохраняйте одинаковые фильтры для всех страниц. Если данные меняются параллельно, повторно проверьте итоговый набор идентификаторов после завершения обхода.
Предотвратить побочные эффекты изменений
Некоторые ошибки проявляются после успешного вызова метода. Перед изменением запишите исходное состояние и проверьте возвращаемое значение или повторное чтение.
Сохранить нужные связи подразделений
Режим replaceExisting: true метода assignToDepartments() затрагивает связи пользователя во всех структурах. Передайте полный набор подразделений, который нужно сохранить, и повторно прочитайте связи после изменения. Порядок назначения описан в статье Пользователи и управленческая иерархия.
Учесть каскад настроек
Методы изменения настроек управляемых подразделений распространяют значение на дочерние узлы. Сравните возвращенные идентификаторы с ожидаемой областью изменения. Повторная запись AiReports = true вызывает событие OnAiReportsEnabled, поэтому учитывайте действия обработчиков. Подробности приведены в разделе Изменить настройки управляемых подразделений.
Правильно интерпретировать список пользователей с правом увольнения
Метод Bitrix\HumanResources\Public\Service\AccessService::getUserIdsWithFirePermission() возвращает активных пользователей, которым право выдано через роль. Он не включает администратора, если право доступно только из-за административного статуса. Используйте результат для анализа ролевых назначений, а не как полный список пользователей с эффективным доступом.
Использовать актуальный API поиска ролей
В классе Bitrix\HumanResources\Public\Service\Node\UserService замените устаревшие методы:
-
findByUserIdAndStructureRoles()— наfindByUserIdAndRoleXmlIds(), -
findAllByUserIdAndStructureRoles()— наfindAllByUserIdAndRoleXmlIds().
Для Bitrix\HumanResources\Builder\Structure\NodeMemberDataBuilder передавайте roleFilter вместо устаревших методов setStructureRoles() и addStructureRole(). Если сочетать их с roleFilter, билдер выбросит \InvalidArgumentException.
Измерить результат оптимизации
Сравнивайте одинаковый сценарий до и после изменения. Один замер без одинаковых входных данных не показывает влияние фильтра, пагинации или кеша.
Класс Bitrix\Main\Diag\SqlTracker измеряет количество и общее время SQL-запросов. Запускайте его непосредственно перед проверяемым вызовом и останавливайте после получения результата. Используйте трекер только в диагностическом сценарии с ограниченным доступом. Собранные SQL-запросы и параметры могут содержать данные проекта. Дополнительные методы описаны в статье Отладка запросов.
Пример. Код измеряет одну страницу подразделений и сохраняет показатели для сравнения.
use Bitrix\HumanResources\Public\Service\Container;
use Bitrix\HumanResources\Type\NodeEntityType;
use Bitrix\Main\Application;
use Bitrix\Main\Loader;
// Подключить модуль
if (!Loader::includeModule('humanresources'))
{
throw new \RuntimeException('Модуль humanresources недоступен');
}
// Подготовить измерение
$structureId = 2;
$connection = Application::getConnection();
$tracker = $connection->startTracker(true);
$startedAt = microtime(true);
$memoryBefore = memory_get_usage(true);
// Выполнить выборку
$nodes = Container::getNodeService()->findAll(
structureId: $structureId,
nodeTypes: [NodeEntityType::DEPARTMENT],
limit: 50,
offset: 0,
);
// Зафиксировать метрики
$duration = microtime(true) - $startedAt;
$memoryDelta = memory_get_usage(true) - $memoryBefore;
$queryCount = $tracker->getCounter();
$queryTime = $tracker->getTime();
$connection->stopTracker();
// Собрать идентификаторы результата
$resultIds = [];
foreach ($nodes as $node)
{
$resultIds[] = $node->id;
}
// Сохранить показатели для сравнения
$measurement = [
'duration' => $duration,
'queryCount' => $queryCount,
'queryTime' => $queryTime,
'memoryDelta' => $memoryDelta,
'resultCount' => count($resultIds),
'resultIds' => $resultIds,
];
Повторите замер с теми же входными данными после изменения. Сравните массивы идентификаторов результата, чтобы оптимизация не изменила состав выборки.
Зафиксируйте для каждого варианта:
-
входные идентификаторы, структуру, типы узлов и фильтры,
-
число выполненных запросов и общее время,
-
количество строк ORM и уникальных объектов результата,
-
размер страницы и число обработанных страниц,
-
состояние кеша и его время жизни,
-
объем памяти, если коллекция обрабатывается целиком.
Оптимизация считается корректной, если она уменьшила измеряемую нагрузку и сохранила ожидаемый набор идентификаторов, порядок и формат результата. После изменения фильтров отдельно проверьте пограничные случаи: пустой вход, последнюю неполную страницу, неактивный узел, команду вместо подразделения и структуру не по умолчанию.