Runtime-поля — это колонки, которых нет в модели, но которые нужны конкретному запросу. Через ExpressionField и registerRuntimeField() вы добавляете агрегаты, вычисления и связи прямо в getList, не трогая getMap() сущности.
Что такое runtime-поле
Runtime-поле объявляется в момент выполнения запроса и живёт только внутри него. Это способ расширить сущность вычисляемой колонкой или связью без изменения её getMap(). Основные виды:
ExpressionField— вычисляемое значение через SQL-выражение;Reference— связь на лету с другой сущностью;- обычные поля — реже, для алиасов и приведения.
Добавить поле можно двумя способами: параметром runtime в getList() или методом registerRuntimeField() объекта Query. Второй способ читаемее для длинных цепочек. Runtime-поле участвует в запросе наравне со штатными: его можно указать в select, использовать в filter, order и group — ORM подставит его выражение в нужную часть SQL.
ExpressionField: синтаксис
Bitrix\Main\ORM\Fields\ExpressionField принимает имя, шаблон SQL с плейсхолдерами и список полей-аргументов. Плейсхолдер %s подставляет реальное имя колонки с нужным JOIN:
use Bitrix\Main\ORM\Fields\ExpressionField;
$fullName = new ExpressionField(
'FULL_NAME',
'CONCAT(%s, " ", %s)',
['NAME', 'LAST_NAME']
);
| Плейсхолдер | Смысл |
|---|---|
%s | Имя поля-аргумента (по порядку из массива) |
%1$s, %2$s | Аргумент по номеру, можно повторять |
Аргументы могут быть полями связанных сущностей (USER.NAME) — ORM сама добавит соединение.
Агрегатные функции
Чаще всего runtime-поля используют для агрегации. ExpressionField со SUM, COUNT, AVG в связке с group заменяет ручной GROUP BY:
$rows = OrderTable::getList([
'select' => ['USER_ID', 'CNT', 'TOTAL'],
'runtime' => [
new ExpressionField('CNT', 'COUNT(*)'),
new ExpressionField('TOTAL', 'SUM(%s)', ['SUM']),
],
'group' => ['USER_ID'],
'filter' => ['>TOTAL' => 0],
])->fetchAll();
Есть и готовые агрегатные хелперы Bitrix\Main\Entity\ExpressionField\..., но в новом коде удобнее статические методы Query:
\Bitrix\Main\ORM\Query\Query::expr()->count('ID');->sum('SUM'),->avg('SUM'),->countDistinct('USER_ID').
filter — ORM сама поймёт, что это HAVING, а не WHERE.Query::expr() — фабрика выражений
Метод Query::expr() возвращает фабрику, которая избавляет от ручного написания SQL для типовых случаев. Пример через объект Query:
use Bitrix\Main\ORM\Query\Query;
$q = OrderTable::query();
$q->registerRuntimeField('TOTAL', Query::expr()->sum('SUM'));
$q->registerRuntimeField('CNT', Query::expr()->count('ID'));
$q->setSelect(['USER_ID', 'TOTAL', 'CNT']);
$q->setGroup(['USER_ID']);
$rows = $q->exec()->fetchAll();
Фабрика поддерживает и произвольные выражения: Query::expr()->concat(...), ->datediff(...). Это читаемее и безопаснее, чем строковые шаблоны, потому что имена полей подставляются корректно. Отдельно отметим ->countDistinct() — он избавляет от дублей при подсчёте по связанной таблице, где обычный count() завысил бы результат из-за размножения строк JOIN'ом.
Связи на лету
Runtime-поле типа Reference добавляет JOIN, которого нет в getMap(). Это спасает, когда сущность чужая или связь контекстно-зависимая:
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
$q = OrderTable::query()
->registerRuntimeField('PAY',
new Reference('PAY', PaymentTable::class,
Join::on('this.ID', 'ref.ORDER_ID')))
->addSelect('PAY.SUM', 'PAID_SUM')
->setFilter(['=PAY.PAID' => 'Y']);
- после регистрации к полям связи обращаетесь через точку:
PAY.SUM; - можно комбинировать с ExpressionField, агрегируя поля связанной таблицы;
- runtime-reference не кэшируется в метаданных сущности — он собирается заново на каждый запрос.
Подзапросы как runtime-поле
Иногда нужен коррелированный подзапрос — например, число дочерних записей без JOIN и группировки основной таблицы. Его тоже оформляют как ExpressionField:
new ExpressionField(
'CHILD_CNT',
'(SELECT COUNT(*) FROM my_child c WHERE c.PARENT_ID = %s)',
['ID']
);
EXPLAIN.Ещё можно подставить целый объект Query как подзапрос — ORM поддерживает вложенные запросы, что удобно для сложной аналитики без сырого SQL.
Итог
Runtime-поля делают ORM D7 по-настоящему гибкой: агрегаты, вычисления, связи и подзапросы добавляются к запросу без правки модели. Используйте ExpressionField и фабрику Query::expr() для читаемых выражений, фильтруйте по агрегатам через тот же filter (ORM превратит его в HAVING) и следите за производительностью коррелированных подзапросов.
Мы в B2Bsite строим аналитические выборки и отчёты на ORM Битрикс, где корректность агрегатов и скорость запросов критичны. Если вашему проекту нужны сложные пользовательские выборки или API поверх них, поможем спроектировать эффективные запросы и оптимизировать нагрузку на БД.
Частые вопросы
Чем runtime-поле отличается от поля в getMap()?
Runtime-поле объявляется в момент запроса и существует только в его рамках, не изменяя определение сущности. Поле в getMap() — постоянная часть модели, доступная всем запросам.
Как передать runtime-поле в getList?
Через параметр runtime — массив объектов ExpressionField или Reference. Альтернатива — метод registerRuntimeField() у объекта Query, который читается лучше для длинных цепочек.
Как отфильтровать результат по агрегатному полю?
Добавьте условие на runtime-агрегат в обычный filter, например ['>TOTAL' => 0]. ORM распознает, что фильтр по агрегату, и сформирует HAVING вместо WHERE.
Что подставляет плейсхолдер %s в ExpressionField?
Реальное имя колонки соответствующего аргумента из массива полей, с корректным префиксом таблицы и нужным JOIN. Нумерованные %1$s, %2$s позволяют повторять аргумент.
Когда использовать Query::expr() вместо строкового шаблона?
Для типовых агрегатов и выражений — count, sum, avg, concat, datediff. Фабрика подставляет имена полей безопасно и делает код читаемее, чем ручные SQL-шаблоны.
Можно ли добавить связь без изменения сущности?
Да, зарегистрируйте runtime-поле типа Reference с нужным Join::on(). Это удобно для чужих сущностей или контекстных связей, но такой JOIN пересобирается на каждый запрос.
Опасны ли подзапросы в ExpressionField?
Коррелированный подзапрос выполняется для каждой строки и на больших выборках сильно замедляет запрос. Сравните его с JOIN плюс GROUP BY и проверьте план выполнения через EXPLAIN.
Поможем с настройкой и поддержкой Для разработчиков
Поможем с настройкой, доработкой и поддержкой Для разработчиков.