← Вернуться к списку статей

Bitrix D7 ORM: забыли про SQL? Нет, используйте его грамотно!

★ ★ ★ ★ ★
Bitrix D7 ORM: забыли про SQL? Нет, используйте его грамотно!

Введение: почему ORM — это не панацея

Когда в Bitrix появилась ORM (Object-Relational Mapping) на базе Doctrine, многие разработчики вздохнули с облегчением. Наконец-то можно работать с объектами, а не писать сырые SQL-запросы вручную! Но как обычно бывает в IT, все не так просто. ORM — это инструмент, который нужно понимать, а не просто «использовать».

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

Почему ORM в Bitrix — это не магия

Bitrix D7 использует Doctrine ORM под капотом. Это значит, что все классы-сущности (например, Bitrix\Iblock\ElementTable) генерируют SQL-запросы автоматически. Понимание этого факта критически важно: если вы не знаете SQL, то и с ORM у вас будут проблемы.

Давайте посмотрим на простой пример — как выглядит запрос в старом стиле (CIBlockElement::GetList) и как это же самое делает ORM:

Старый стиль

$arFilter = [
    'ACTIVE' => 'Y',
    'ID' => ['=' => 123],
];
$element = CIBlockElement::GetList(
    $arSort = ['SORT' => 'ASC'],
    $arFilter,
    ['COUNT' => 'N'],
    false,
    ['ID', 'NAME']
);
$result = $element->Fetch();

Новый стиль (ORM)

$result = Bitrix\Iblock\ElementTable::getList([
    'filter' => [
        'ACTIVE' => 'Y',
        'ID' => ['=' => 123],
    ],
    'order' => ['SORT' => 'ASC'],
    'select' => ['ID', 'NAME'],
]);

Видите разницу? ORM просто переупаковывает этот запрос в объектно-ориентированную форму. Под капотом — то же самое.

Ошибка №1: «Наивный» выбор данных

Самая распространённая ошибка новичков — выборка всех полей бездумно:

// ❌ ПЛОХО: выбираем всё, даже если нужно только 2 поля
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => '*',
]);

Это может привести к двойной загрузке базы (одна выборка для данных, вторая — для свойств). Правильный подход:

// ✅ ХОРОШО: выбираем только то, что нужно
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => [
        'ID',
        'NAME',
        'PROPERTY_CODE',
        'IBLOCK_SECTION_ID',
    ],
]);

В Bitrix D7 свойства элементов хранятся в отдельной таблице b_iblock_element_properties. Если вы не указываете поля свойств явно, ORM всё равно попытается их загрузить.

Ошибка №2: N+1 проблема (классика)

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

// ❌ ПЛОХО: цикл + отдельный запрос на каждую итерацию
$elements = Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME'],
]);

foreach ($elements as $element) {
    // Каждый раз — новый SQL-запрос к базе!
    $properties = Bitrix\Iblock\PropertyTable::getList([
        'filter' => [
            'IBLOCK_ELEMENT_ID' => $element->getId(),
        ],
    ]);
}

Если у вас 100 элементов — это 100 запросов к базе. Это называется N+1 проблема (1 запрос на список + N запросов по одному элементу). Результат — медленный сайт, особенно при больших списках.

Как решить: предзагрузка через join

// ✅ ХОРОШО: один запрос с JOIN-ом
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => [
        'ID',
        'NAME',
        ['IBLOCK_ELEMENT_PROPERTY' => ['*' => 'Y']], // загрузить все свойства сразу
    ],
]);

Или, если нужны конкретные свойства:

$result = Bitrix\Iblock\ElementTable::getList([
    'select' => [
        'ID',
        'NAME',
        ['IBLOCK_ELEMENT_PROPERTY' => ['CODE' => ['*' => 'Y']]],
    ],
]);

В этом случае Doctrine генерирует один SQL-запрос с JOIN-ами, что в разы быстрее.

Ошибка №3: Работа со сложными связями

Bitrix D7 поддерживает сложные связи между таблицами. Например, у элемента может быть много свойств, и каждое свойство — это отдельная запись в таблице.

Вот как правильно работать с вложенными данными:

// ✅ Правильный подход: nested select
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => [
        'ID',
        'NAME',
        [
            'IBLOCK_ELEMENT_PROPERTY' => [
                'CODE' => ['*' => 'Y'],
                'VALUE' => '*',
            ],
        ],
    ],
]);

Это сгенерирует один запрос вида:

SELECT 
    e.ID, e.NAME,
    p.CODE, p.VALUE
FROM b_iblock_element AS e
LEFT JOIN b_iblock_element_property AS p ON p.IBLOCK_ELEMENT_ID = e.ID

Ошибка №4: Игнорирование индексов

Bitrix D7 автоматически создаёт индексы для часто используемых полей, но не всегда. Если вы делаете поиск по полю PROPERTY_VALUE, а оно не индексировано — запрос будет медленным.

Как проверить:

$db = \Bitrix\Main\DB::getConnection();
$sql = $db->query('SHOW INDEX FROM b_iblock_element_property');
$result = $sql->fetchAll(\PDO::FETCH_ASSOC);
print_r($result);

Если индекс отсутствует — нужно добавить его вручную через миграции или напрямую в базу.

Ошибка №5: Работа с фильтрами и группировкой

В старом стиле Bitrix был привычный параметр 'FIELDS' => ['ID', 'NAME']. В ORM это работает иначе:

// ❌ ПЛОХО
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => '*',
    'group' => 'IBLOCK_SECTION_ID', // не сработает корректно!
]);

Правильный подход:

// ✅ ХОРОШО
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME'],
    'group' => ['IBLOCK_SECTION_ID' => true],
]);

Или при агрегации:

$result = Bitrix\Iblock\ElementTable::getList([
    'select' => [
        'ID',
        'NAME',
        ['IBLOCK_SECTION_ID' => \Bitrix\Main\DB::expr('COUNT(*)')],
    ],
    'group' => ['IBLOCK_SECTION_ID'],
]);

Ошибка №6: Забытые JOIN-ы в связях

Если вы работаете с разделами (сегментами) или категориями, и просто делаете выборку по ID — вы получите данные без связей. Пример:

// ❌ ПЛОХО: не видим связь элемент → раздел
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME'],
]);

Правильно:

// ✅ ХОРОШО: явно указываем связь
$result = Bitrix\Iblock\ElementTable::getList([
    'select' => [
        'ID',
        'NAME',
        ['IBLOCK_SECTION_ID' => true], // явный JOIN на разделы
    ],
]);

Когда лучше использовать чистый SQL

ORM — это хорошо, но не всегда. Вот ситуации, когда стоит писать SQL вручную:

  1. Сложные агрегации — группировка по нескольким полям с подзапросами.
  2. Огромные выборки — если нужно прочитать миллионы строк, ORM будет генерировать избыточные JOIN-ы.
  3. Специфические SQL-функции — например, оконные функции, которые Doctrine не поддерживает «из коробки».
  4. Кастомные индексы и оптимизация — если вы знаете, что нужно добавить конкретный индекс для ускорения запроса.

Практический пример: поиск товаров с фильтрами

// Поиск товаров в категории с фильтром по цене и наличию
$result = Bitrix\Iblock\ElementTable::getList([
    'filter' => [
        'IBLOCK_SECTION_ID' => ['=' => 5],
        'PROPERTY_PRICE_FROM' => ['