Получение дополнительных данных для списка элементов

Урок 9 из 9

30 мин

Дополнительный материал от команды Академии 1С-Битрикс.

По теме кастомизации типовых компонентов обратим внимание начинающих разработчиков на несколько аспектов:

  • Изучайте возможности часто используемых API.
  • Избегайте использования запросов в цикле, если можно обойтись без них.
  • Если объем получаемых данных можно ограничить на уровне выборки в вызове API, то следует так и сделать, а не выбирать данные с «запасом» и фильтровать затем в PHP.
  • Не получайте одни и те же данные повторно через API, если при этом выполняется запрос в базу данных. Сохраните в PHP ранее полученные значения и используйте их для решения задачи.
  • Проверяйте значения переменных перед их использованием в фильтре для выбора элементов.
  • По умолчанию при получении элементов используйте фильтр по активности.

Получение данных через API в шаблонах компонентов

Одна из частых задач, решаемых кастомизацией компонентов, — это получение дополнительных данных с помощью API в result_modifier.php. для элементов информационного блока в популярных компонентах, выводящих список новостей, статей, товаров и т.д.

Применяются API как "старого" ядра, так и "нового" D7. Имеет смысл хорошо разобраться со всеми параметрами регулярно используемых методов, таких как получение элементов информационного блока, пользователей, пользовательских свойств и т.д.

Кроме того, обратите внимание, как работать в API с данными типа дата, список, файл.

Метод GetList класса CIBlockElement, возможно, самый используемый в Bitrix Framework за всю историю существования платформы :) Возможностей у него много, неочевидные примеры: может вернуть количество элементов по фильтру без необходимости считать в PHP или выполнить подзапрос для сложного фильтра.

Выборка связанных данных без запросов в цикле и получение сразу только нужных данных

Выполняя даже простые задачи, имеет смысл помнить об эффективности вашего алгоритма.

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

Для исследования темы используем полезнейший инструмент "Отладка". Он покажет количество запросов, которые создаёт компонент.

картинка

Посмотрим на работу компонента news.list, выводящего список элементов в простом шаблоне:

картинка

Он генерирует всего 4 запроса:

картинка

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

Для простоты и наглядности используем "старое" API.

Предположим, есть инфоблок с дополнительными данными, "Обзоры". У каждого обзора создано поле для связи с новостями, множественное.

картинка

При получении данных для списка элементов типичная ошибка выглядит так, "решение в лоб": в result_modifier.php в цикле по списку элементов с помощью GetList получить связанные элементы.

foreach ($arResult["ITEMS"] as $key => $arItem)
{
    $arResult["ITEMS"][$key]["EXTRA"] = [];
    $res = CIBlockElement::GetList(
        ["ID" => "ASC"],
        [
            "IBLOCK_ID" => ID_IBLOCK_EXTRA, 
            "ACTIVE" => "Y", 
            "PROPERTY_NEWS" => $arItem["ID"]
        ],
        false,
        false,
        [
            "ID", 
            "IBLOCK_ID", 
            "NAME", 
        ]
    );
    while ($row = $res->GetNext())
    {
        $arResult["ITEMS"][$key]["EXTRA"][] = $row;
    }
}

В шаблоне возле текста анонса добавить вывод

<?if(count($arItem["EXTRA"]) > 0):?>
    <p>Дополнительные материалы:</p>
    <ul>
    <?foreach($arItem["EXTRA"] as $key => $itemExtra ):?>
        <li><?=$itemExtra["NAME"]?> </li>
    <?endforeach;?>
    </ul>
<?endif?>

Получится вот так:

картинка

Можно увидеть, что запросов стало 16, время на их исполнение заметно увеличилось, и это всего на 10 новостей в списке и простейшем доборе данных. Чем больше элементов выводится, чем больше данных нужно, тем больше будет запросов и затраты времени на выполнение.

Для бОльшей наглядности добавим условие: не все обзоры нужно выводить. Например, нужны только те, у которых в дополнительном поле указан пользователь — автор, у которого пользовательское поле флаг "Проверен" активно. В примере используем двух авторов, один из них с нужным флажком.

картинка
картинка

Это значит, что в списке новостей должен быть выведен только "Обзор 1" у трех новостей.

Если продолжать решать задачу "в лоб", то получится еще один цикл с запросом, генерируемым GetByID внутри ранее созданного цикла:

foreach ($arResult["ITEMS"] as $key => $arItem)
{
    $arResult["ITEMS"][$key]["EXTRA"] = [];
    $res = CIBlockElement::GetList(
        ["ID" => "ASC"],
        [
            "IBLOCK_ID" => ID_IBLOCK_EXTRA,
            "ACTIVE" => "Y",
            "PROPERTY_NEWS" => $arItem["ID"]
        ],
        false,
        false,
        [
            "ID",
            "IBLOCK_ID",
            "NAME",
            "PROPERTY_AUTHOR",
        ]
    );
    while ($row = $res->GetNext())
    {
        $rsUser = CUser::GetByID($row['PROPERTY_AUTHOR_VALUE']);
        if ($arUser = $rsUser->GetNext())
        {
            if($arUser["UF_VERIFIED"])
            {
                $arResult["ITEMS"][$key]["EXTRA"][] = $row;
            }
        }
    }
}

Это решение уже даст 30 запросов:

картинка

Кроме того, если у обзоров автор будет повторяться (что ожидаемо), то будут повторяться одинаковые запросы на получение данных, которые уже пришли в PHP, что совсем не оптимально. А если автор не подходит под условие, то его обзоры вовсе не нужно было отбирать изначально.

Можно по-разному оптимизировать ситуацию, используем простой и наглядный пошаговый подход:

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

Получится так:

$itemIDs = [];
foreach ($arResult["ITEMS"] as $key => $arItem)
{
    $itemIDs[] = $arItem["ID"];
}

$authorIDs = [];
$sortBy = "id";
$sortOrder = "asc";
$arParams["FIELDS"] = ["ID"];
$filter = ["ACTIVE" => "Y", "UF_VERIFIED" => true];
$res = CUser::GetList(
    $sortBy,
    $sortOrder,
    $filter,
    $arParams
);
while ($row = $res->GetNext())
{
    $authorIDs[] = $row["ID"];
}

if(
    (count($itemIDs) > 0)
    &&
    (count($authorIDs) > 0)
)
{
    $arResult["EXTRA"] = [];
    $res = CIBlockElement::GetList(
        ["ID" => "ASC"],
        [
            "IBLOCK_ID" => ID_IBLOCK_EXTRA,
            "ACTIVE" => "Y",
            "PROPERTY_NEWS" => $itemIDs,
            "PROPERTY_AUTHOR" => $authorIDs,
        ],
        false,
        false,
        [
            "ID",
            "IBLOCK_ID",
            "NAME",
            "PROPERTY_NEWS",
        ]
    );
    while ($row = $res->GetNext())
    {
        $arResult["EXTRA"][$row["PROPERTY_NEWS_VALUE"]][] = $row;
    }
}

Модифицируем вывод в шаблоне:

<?if(isset($arResult["EXTRA"][$arItem["ID"]])):?>
<p>Дополнительные материалы:</p>
<ul>
    <?foreach($arResult["EXTRA"][$arItem["ID"]] as $key => $itemExtra ):?>
    <li>
        <?=$itemExtra["NAME"]?>
    </li>
    <?endforeach;?>
</ul>
<?endif?>

Смотрим статистику, запросов стало значительно меньше:

картинка

Запросов не 6, как можно было бы ожидать (4 штатных + 2 наших кастомных, по 1 на каждый GetList в result_modifier.php). GetList может формировать больше одного запроса к базе данных. Не указывайте в фильтре и выборке поля, которые не используете по факту в коде, особенно пользовательские.

Теперь, даже если вывести 30 новостей, запросов остаётся 10.

картинка

Исходное решение дало бы 50 запросов на 30 новостей, будет еще больше элементов - станет еще больше запросов.

картинка

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

Проверка на пустые значения переменных в фильтре

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

if(
    (count($itemIDs) > 0)
    &&
    (count($authorIDs) > 0)
)

Это не только ради экономии запроса, если нет новостей для выбора или подходящих авторов. Если передать пустые массивы в фильтр в старом API, то это аналогично отсутствию фильтра, будут выбраны все элементы, а не 0. Что полностью нарушает решение задачи.

По умолчанию выборка включает фильтр по активности

Хорошим тоном в выборке по умолчанию добавлять фильтр по активным элементам, если из задачи явно не следует другого.

$res = CIBlockElement::GetList(
    ["ID" => "ASC"],
    [
        ...
        "ACTIVE" => "Y",
        ...
    ],
    false,
    false,
    [
        "ID",
        "NAME",
    ]
);

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

картинка

Полезно

Узнайте больше о получении данных через API, кешировании и оптимизации производительности в курсе "Свои сущности на базе Bitrix Framework".