Как построить сложную выборку D7 ORM с ExpressionField в 1С-Битрикс?

Как построить сложную выборку D7 ORM с ExpressionField в 1С-Битрикс?

Отчёт "топ-10 пользователей по сумме заказов за месяц" превратился в 15 000 строк getList и 501 SQL-запрос в foreach - сайт лёг по таймауту. После обновления ядра попытка вставить SUM(AMOUNT) в select дала ArgumentException: Expression as an array in select section is no more supported. Решение - Table::query() с ExpressionField: агрегация COUNT/SUM уходит в один SQL, фильтр по итогу - в HAVING автоматически. Ниже пошаговый гайд для 1С-Битрикс: Управление сайтом без выгрузки всего набора в PHP.

Table::query() нужен, когда фильтры и поля собираются динамически в сервисе модуля, а не статическим массивом getList. ExpressionField и registerRuntimeField описывают SUM/COUNT/CONCAT объектом, не строкой SQL. Фильтр по агрегату (>CNT) ORM переносит в HAVING, неагрегированные поля из select добавляет в GROUP BY. Отладка: getQuery() покажет SQL без выполнения запроса. Результат: один запрос вместо N+1 в цикле.

Речь про коробочную CMS, не про CRM-портал Bitrix24. ORM (Object-Relational Mapping) - слой D7, который связывает PHP-класс *Table с таблицей MySQL. Свою таблицу вы уже создали в гайде по DataManager и getMap (B71). Простую выборку инфоблока разобрали в сравнении GetList и ElementTable (B60). Здесь - сложные отчёты: агрегация, группировка и отладка SQL.

Выберите, когда нужен Table::query(), а когда хватит getList

Сравнительная таблица getList и Table::query() для сценариев Битрикс D7 ORM

getList с массивом filter/select/order - нормальный выбор для фиксированной выборки из компонента. На практике Query выигрывает, когда условия собираются по шагам в сервисе модуля: сегодня фильтр по дате, завтра - по статусу и сумме, плюс runtime-поле COUNT без правки getMap.

Сценарий getList-массив Table::query()
Фиксированный filter в компоненте Да - короче и привычнее Избыточно
Динамические addFilter по GET-параметрам Каша вложенных if Да - читаемый сервис
SUM/COUNT по группе USER_ID Только через runtime + group Да - registerRuntimeField + setGroup
JOIN с именем из другой таблицы runtime Reference Да, плюс гайд по Reference и JOIN (B79)
CRUD Highload-справочника getList HL DataClass Редко - см. CRUD Highload через D7 (B78)

Делайте: переносите сборку условий в lib/Service/, а не в шаблон компонента. Не делайте: не тяните 10 000 строк в PHP, чтобы посчитать SUM в foreach - БД сделает это быстрее одним запросом.

Соберите Table::query() с динамическими фильтрами пошагово

Workflow-схема сборки Table::query() с динамическими фильтрами в D7 ORM

OrderLogTable::query() - флюент-обёртка над Entity\Query. Методы addSelect, addFilter (или where), setOrder, setLimit и setOffset цепляются по условию.

  1. Создайте $query = OrderLogTable::query();
  2. Добавьте скалярные поля: $query->addSelect('USER_ID')->addSelect('STATUS');
  3. Задайте базовый фильтр по дате: $query->where('DATE_CREATE', '>=', $dateFrom);
  4. Условно добавьте фильтр по статусу: if ($status) { $query->where('STATUS', $status); }
  5. Установите сортировку и пагинацию: setOrder(['TOTAL' => 'DESC'])->setLimit(10)->setOffset($page * 10);
  6. Выполните $result = $query->exec(); и обходите fetch() без fetchAll на весь набор.
$query = OrderLogTable::query()
    ->addSelect('USER_ID')
    ->where('DATE_CREATE', '>=', $dateFrom);

if ($status !== '') {
    $query->where('STATUS', $status);
}

$result = $query
    ->setOrder(['USER_ID' => 'ASC'])
    ->setLimit(50)
    ->setOffset($offset)
    ->exec();

Workflow отчёта: сервис собирает query → registerRuntimeField → setGroup → фильтр по агрегату → getQuery() на staging → exec() на prod.

Делайте: setLimit/setOffset для пагинации. Не делайте: fetchAll() на десятки тысяч строк.

Настройте ExpressionField и registerRuntimeField для SUM и COUNT

Чеклист настройки ExpressionField и registerRuntimeField для агрегации SUM COUNT

ExpressionField - виртуальное поле: имя для PHP, SQL-выражение для БД. Плейсхолдер %s подставляет имя колонки из buildFrom. С версии main 12.0.0 поле регистрируют на Query через registerRuntimeField - эквивалент runtime в getList.

use Bitrix\Main\ORM\Fields\ExpressionField;

$query = OrderLogTable::query()
    ->registerRuntimeField(
        'TOTAL',
        new ExpressionField('TOTAL', 'SUM(%s)', 'AMOUNT')
    )
    ->registerRuntimeField(
        'CNT',
        new ExpressionField('CNT', 'COUNT(*)')
    )
    ->addSelect('USER_ID')
    ->addSelect('TOTAL')
    ->addSelect('CNT');

Агрегаты COUNT, SUM, AVG, MAX, MIN и CONCAT работают в одном запросе. Для дат между MySQL и PostgreSQL берите SqlHelper вместо NOW() напрямую.

Типичная ошибка после PHP 8 / ORM 2.0: массив ['SUM(%s)', 'AMOUNT'] или строка 'SUM(AMOUNT)' в select дают ArgumentException: Expression as an array in select section is no more supported. Замена - только объект ExpressionField или registerRuntimeField.

Делайте: давайте runtime-полям короткие alias в верхнем регистре (TOTAL, CNT). Не делайте: не копируйте с форумов сниппеты со строкой SQL в select - они сломаются на свежем ядре.

Настройте GROUP BY и HAVING без ручного SQL

Когда в select есть агрегат, ORM группирует по неагрегированным полям. Явная группировка: setGroup(['USER_ID']). Фильтр по скаляру до группировки уходит в WHERE, по агрегату - в HAVING.

$query = OrderLogTable::query()
    ->registerRuntimeField('TOTAL', new ExpressionField('TOTAL', 'SUM(%s)', 'AMOUNT'))
    ->registerRuntimeField('CNT', new ExpressionField('CNT', 'COUNT(*)'))
    ->addSelect('USER_ID')
    ->addSelect('TOTAL')
    ->addSelect('CNT')
    ->setGroup(['USER_ID'])
    ->where('CNT', '>', 5)
    ->where('TOTAL', '>', 10000)
    ->setOrder(['TOTAL' => 'DESC'])
    ->setLimit(10);

Ловушка: лишний ID в select вместе с SUM добавит ID в GROUP BY и "размажет" агрегат. В отчёте только ключ группировки плюс агрегаты. Query::expr()->length() и expr()->count() заменяют ручной ExpressionField для типовых функций.

Делайте: фильтруйте агрегат через alias runtime-поля (>CNT). Не делайте: не пишите HAVING вручную в Connection::query - ORM уже маршрутизирует условие.

Соберите отчёт одним запросом вместо N+1 в foreach

Антипаттерн: getList на каждый orderId в цикле. Верно: один query с setGroup('USER_ID'), SUM(AMOUNT) и setLimit(10). Поля из связанной таблицы - runtime Reference в том же query (B79), не отдельный запрос в foreach.

Делайте: перед релизом посчитайте число SQL в профайлере или SqlTracker. Не делайте: не оправдывайте N+1 тем, что "всего 200 заказов" - на проде их станет 20 000.

Проверьте SQL через getQuery() до выполнения на prod

getQuery() вернёт SQL без exec(). SqlTracker на Connection: startTracker(), getList() или exec(), getQueries() - покажет фактический запрос. Сверьте GROUP BY и HAVING на staging с тем же объёмом данных, что на prod.

Делайте: getQuery() перед exec(). Не делайте: не правьте агрегат вслепую на боевом.

Пройдите чек-лист перед выкладкой отчёта на prod

  1. Проверьте агрегацию в SQL, не в foreach.
  2. Убедитесь, что ExpressionField - объект, не строка в select.
  3. Уберите лишние поля из select, кроме ключей группировки.
  4. Включите setLimit/setOffset вместо fetchAll.
  5. Сверьте getQuery() на staging: GROUP BY, HAVING, индексы на полях фильтра.

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

Автор: Максим Мольков, Senior-разработчик 1С-Битрикс.
Источники: выборка данных ORM Bitrix Framework, построитель запросов Query, ExpressionField D7 API, урок по GROUP BY в ORM.

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

Чем Table::query() отличается от getList с массивом?

getList принимает готовый массив filter/select/order - удобно для статики. Table::query() позволяет по шагам addSelect, where, registerRuntimeField и setGroup в сервисе. Для отчётов с агрегацией query читается проще, чем вложенные if вокруг массива getList.

Как посчитать SUM или COUNT через ExpressionField?

На Query вызовите registerRuntimeField с new ExpressionField('TOTAL', 'SUM(%s)', 'AMOUNT') и addSelect('TOTAL'). Для COUNT - ExpressionField('CNT', 'COUNT(*)'). Фильтр where('CNT', '>', 5) ORM перенесёт в HAVING. Не вставляйте строку SUM(AMOUNT) в select - только объект поля.

Почему select со строкой SQL больше не работает?

С ORM 2.0 и PHP 8 массив или строка в select блокируются: ArgumentException про security reason. Замените на ExpressionField или registerRuntimeField. Это защита от инъекций через "выражения" в select.

Как отладить SQL запроса ORM без выполнения?

После сборки query вызовите getQuery() - вернётся строка SQL. Для уже выполненного getList включите SqlTracker на Connection, выполните запрос, прочитайте getQueries() и getSql(). Сверьте GROUP BY и HAVING до выкладки на prod.

Нужен ли явный GROUP BY для агрегации в ORM?

При фильтре по агрегату ORM часто добавляет GROUP BY сам. Для отчёта лучше явно setGroup(['USER_ID']) - так предсказуемее. Помните: каждое неагрегированное поле в select попадёт в GROUP BY, лишний ID разобьёт сумму по строкам.

Работает ли ExpressionField с Highload-блоком?

Да - HL DataClass после compileEntity тоже *Table. registerRuntimeField и setGroup применимы к getList и query HL-сущности. Базовый CRUD HL - в отдельном гайде B78; здесь фокус на агрегации и Query для любой ORM-таблицы.

Читайте также

Интеграции с 1С и API
1073 6 мин.

Как настроить регистрацию и личный кабинет покупателя на 1С-Битрикс?

Пошаговая настройка регистрации на сайте битрикс: Главный модуль, main.register, system.auth.form и sale.personal.section с историей заказов и 152-ФЗ.
Интеграции с 1С и API
941 6 мин.

Как настроить скидки и промокоды на 1С-Битрикс: правила корзины и купоны

Пошаговый гайд по скидкам в CMS-магазине: скидка на товар, правило корзины от суммы, купоны с лимитом, приоритеты без конфликтов и тестовый заказ. Не Bitrix24.
Интеграции с 1С и API
757 15 мин.

Что такое компонент в Битриксе и как он работает

Каждый разработчик, впервые столкнувшийся с Битриксом, проходит через своеобразный обряд посвящения. Вначале кажется, что это просто CMS, где можно поправить HTML в визуальном редакторе или дописать пару строк CSS. Но однажды наступает момент, когда нужно изменить логику вывода новостей, отфильтровать товары по хитрому свойству или добавить на страницу нечто совершенно новое. И тут он впервые слышит это слово — «компонент». Для многих этот момент становится стеной. Система, казавшаяся понятной, вдруг превращается в черный ящик, полный непонятных файлов и странных переменных. Но стоит лишь раз заглянуть под капот, как эта стена рассыпается, превращаясь в набор удивительно логичных и мощных строительных блоков. Понимание компонентов — это тот самый щелчок, после которого разработка на Битрикс из мучения превращается в творчество.

Эта статья — ваш проводник в мир компонентов «1С-Битрикс». Мы не будем сыпать сухими терминами из документации. Вместо этого мы совершим путешествие: от философии, заложенной в эту архитектуру, до мельчайших деталей её работы. Мы разберем компонент на атомы — его файлы, логику, шаблон, параметры — и соберем обратно, чтобы вы не просто знали, что это, но и глубоко понимали, почему это работает именно так. Это знание — ключ к эффективной и профессиональной разработке на Битрикс.

Интеграции с 1С и API
717 2 мин.

Установка Composer в 1С-Битрикс

Установка Composer в проекте на 1С-Битрикс требует учета особенностей платформы, чтобы обеспечить корректную работу и интеграцию с системой.