Перейти к содержимому

Выборки из инфоблоков - GetList, ORM и разделы

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

Четыре параметра старого ядра

Выборка старого ядра принимает пять аргументов, и почти все ошибки живут в порядке их следования:

$res = CIBlockElement::GetList(
['SORT' => 'ASC', 'NAME' => 'ASC'], // 1. сортировка
['IBLOCK_ID' => 5, 'ACTIVE' => 'Y'], // 2. фильтр
false, // 3. группировка
['nPageSize' => 20], // 4. постраничная навигация
['ID', 'NAME', 'PROPERTY_MATERIAL'] // 5. что выбрать
);
while ($row = $res->GetNext()) {
printf("%-6s %-30s %s\n", $row['ID'], $row['NAME'], $row['PROPERTY_MATERIAL_VALUE']);
}

Третий аргумент - группировка, а не количество строк. Именно сюда по ошибке кладут ограничение выборки, и вместо двадцати элементов приходит одно число: результат группировки.

Пятый аргумент задаёт список полей. Всё, чего в нём нет, платформа дочитает отдельным запросом при первом же обращении - молча и по одному запросу на строку.

Активность элемента - не одно поле, а три:

$filter = [
'IBLOCK_ID' => 5,
'ACTIVE' => 'Y',
'ACTIVE_DATE' => 'Y', // учитывает даты начала и окончания публикации
];

Элемент с флагом активности и датой окончания в прошлом остаётся активным по первому полю и невидимым для посетителя. Проверка по датам включается отдельно, и именно её забывают, когда «товар активен, но его нет на витрине».

Ограничение количества

Простого параметра «сколько строк» у старого ядра нет. Ограничение задаётся через постраничную навигацию:

$res = CIBlockElement::GetList([], $filter, false,
['nTopCount' => 10], // первые десять строк
['ID', 'NAME']);
$res = CIBlockElement::GetList([], $filter, false,
['nPageSize' => 20, 'iNumPage' => 3], // третья страница по двадцать
['ID', 'NAME']);

Первая форма даёт ограничение сверху, вторая - страницу. Смешивать их в одном вызове бессмысленно: побеждает первая, а вторая молча игнорируется.

Те же задачи на ORM

Новое ядро описывает выборку объектом запроса:

use Bitrix\Iblock\ElementTable;
$rows = ElementTable::getList([
'select' => ['ID', 'NAME', 'IBLOCK_SECTION_ID'],
'filter' => ['=IBLOCK_ID' => 5, '=ACTIVE' => 'Y'],
'order' => ['SORT' => 'ASC'],
'limit' => 20, // ограничение здесь штатное
])->fetchAll();

Знак равенства в фильтре - часть имени поля, а не опечатка. Он задаёт способ сравнения: без него платформа выбирает его сама, и на строковых полях это может оказаться сравнение по образцу.

Свойства в ORM не приходят сами. Их выбирают через таблицу значений или через скомпилированную сущность инфоблока:

use Bitrix\Iblock\Model\Section;
$entity = \Bitrix\Iblock\Iblock::wakeUp(5)->getEntityDataClass();
$rows = $entity::getList([
'select' => ['ID', 'NAME', 'MATERIAL_' => 'MATERIAL'], // свойство как поле
'filter' => ['=ACTIVE' => 'Y'],
])->fetchAll();

Скомпилированная сущность превращает свойства инфоблока в обычные поля запроса. Это единственный способ фильтровать и сортировать по свойству без ручных соединений, но он требует, чтобы у инфоблока был включён нужный режим хранения.

Разделы элемента

Поле раздела в элементе хранит основную привязку. Полный список разделов живёт в таблице связи:

use Bitrix\Iblock\SectionElementTable;
$links = SectionElementTable::getList([
'select' => ['IBLOCK_ELEMENT_ID', 'IBLOCK_SECTION_ID'],
'filter' => ['@IBLOCK_ELEMENT_ID' => $elementIds], // сразу по списку элементов
])->fetchAll();
$byElement = [];
foreach ($links as $link) {
$byElement[$link['IBLOCK_ELEMENT_ID']][] = $link['IBLOCK_SECTION_ID'];
}

Один запрос по списку идентификаторов заменяет цикл с запросом на каждый элемент. На выборке в сто товаров это разница между двумя запросами и сто одним, и на витрине она видна невооружённым глазом.

Адрес элемента собирается по основной привязке. Товар, лежащий в трёх разделах, получает один адрес - по тому разделу, который указан первым, и остальные разделы на адрес не влияют.

Подсчёт количества

Посчитать элементы можно тремя способами, и они дают разный SQL:

// 1. счётчик готовой выборки: строки уже выбраны из базы
$res = CIBlockElement::GetList([], $filter, false, false, ['ID']);
echo $res->SelectedRowsCount(), "\n";
// 2. группировка вместо выборки: база возвращает одно число
$count = CIBlockElement::GetList([], $filter, []);
// 3. ORM: подсчёт заказывается явно
$count = ElementTable::getCount(['=IBLOCK_ID' => 5, '=ACTIVE' => 'Y']);

Первый способ тянет из базы все строки и считает их в памяти. На каталоге в десятки тысяч позиций это заметно на времени ответа и на потреблении памяти, а результат ничем не отличается от второго и третьего.

Второй способ и есть тот самый третий аргумент выборки. Пустой массив в нём означает группировку без полей, то есть подсчёт по всему фильтру. Именно поэтому случайно переданное туда ограничение превращает выборку в одно число.

Права и выборка

Выборка элементов учитывает права текущего пользователя. Это удобно на витрине и мешает в агентах и консольных скриптах, где текущего пользователя нет:

$res = CIBlockElement::GetList([], [
'IBLOCK_ID' => 5,
'CHECK_PERMISSIONS' => 'N', // выбрать всё, минуя проверку прав
], false, false, ['ID', 'NAME']);

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

Разница между запуском из админки и из агента объясняется тем же. Администратор видит всё, агент работает без пользователя, и одна и та же выборка возвращает разное число строк в зависимости от того, кто её запустил.

Откуда берутся лишние запросы

Ленивое дочитывание - главный источник нагрузки в старом ядре. Метод получения строки с обработкой возвращает поля, которых не было в списке выбранных, и делает это отдельным запросом на каждую строку.

// плохо: DETAIL_PAGE_URL и свойства не заказаны, но используются
$res = CIBlockElement::GetList([], $filter, false, false, ['ID', 'NAME']);
while ($row = $res->GetNext()) {
echo $row['DETAIL_PAGE_URL']; // + запрос на каждой строке
}
// хорошо: всё нужное перечислено сразу
$res = CIBlockElement::GetList([], $filter, false, false,
['ID', 'NAME', 'DETAIL_PAGE_URL', 'PROPERTY_MATERIAL']);

Разница видна в отладке запросов: у первого варианта их столько же, сколько элементов, у второго - один. Правило простое: всё, что читается в цикле, должно быть названо в списке полей.

Кэширование выборок живёт отдельно от самих выборок. Компонент кэширует свой результат целиком, а прямой вызов в коде страницы не кэшируется никак, сколько бы раз он ни повторялся на одной странице.

Что выбрать под свою задачу

Старое ядро выигрывает там, где нужны вычисляемые поля. Адрес элемента, адрес раздела, картинка предпросмотра в готовом виде - всё это платформа собирает сама и отдаёт вместе со строкой. В ORM те же значения приходится собирать своим кодом по шаблонам из настроек инфоблока.

ORM выигрывает там, где выборка перестаёт быть простой. Соединение с чужой таблицей, подзапрос, своё выражение в списке полей, предсказуемый SQL под конкретный индекс - всё это в старом ядре либо невозможно, либо делается через прямые запросы к базе мимо API.

Смешивать оба API в одном месте кода не запрещено, и на практике так и выходит: список товаров собирается старым вызовом ради адресов, а связанные с ним данные дочитываются одним запросом ORM. Плохо здесь только одно - когда одна и та же сущность читается двумя способами в соседних строках, и через полгода никто не помнит, почему именно так.

Прямые запросы к таблицам инфоблоков - последнее средство, а не альтернатива. Структура таблиц меняется между версиями продукта, и обновление ломает такой код без предупреждения. Там, где без прямого запроса не обойтись, его прячут в одну функцию и снабжают комментарием, что именно в API не нашлось.

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

Типичные проблемы

Вместо элементов приходит одно число.

Ограничение выборки положено в третий аргумент. Это группировка, и её наличие превращает выборку в подсчёт.

Элемент активен, но на витрине его нет.

В фильтре не задана проверка дат публикации. Флаг активности и даты - разные условия, и второе проверяется отдельно.

Страница делает сотни запросов к базе.

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

Фильтр по свойству в ORM не работает.

Свойства не приходят в обычной выборке элементов. Их берут через скомпилированную сущность инфоблока или через таблицу значений.

У элемента виден только один раздел.

Читается поле основной привязки. Полный список разделов лежит в таблице связи элементов и разделов.

Частые вопросы

Устарел ли CIBlockElement::GetList?

Нет, он поддерживается и остаётся удобным там, где нужны вычисляемые поля вроде адреса элемента. ORM выигрывает на соединениях и на предсказуемости запроса.

Как выбрать элементы без вложенных разделов?

Фильтром по конкретному разделу без учёта подразделов: за это отвечает отдельное условие выборки. По умолчанию платформа берёт и вложенные разделы тоже.

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

Штатной сортировкой нет: она работает по полям. Порядок задают отдельным числовым свойством либо сортируют массив после выборки.

Почему одна и та же выборка возвращает разное число строк?

В фильтре не задана проверка дат публикации, и часть элементов уходит по времени. Второй вариант - разные права у пользователя, от имени которого идёт выборка.

Смежное

Первоисточник