Когда нужно вытащить из базы список заказов за период, отсортировать товары по остаткам или подтянуть данные из связанной таблицы, встаёт выбор: писать SQL руками или использовать d7 orm выборки Битрикс. За несколько лет работы с этой платформой я перешёл на D7 почти везде, где раньше писал CIBlockElement::GetList или прямые запросы через CDatabase. Ниже разбираю фильтры по датам, сортировку через order, связи между таблицами и грабли, на которые сам наступал на боевых проектах.
Что такое D7 ORM и чем он отличается от старого API
D7 появился в ядре Битрикс как замена старым классам вида CIBlockElement, CSaleOrder и прямым SQL-запросам через CDatabase. Вместо процедурного стиля с ручной сборкой строки запроса используются классы-наследники Bitrix\Main\ORM\Data\DataManager: Bitrix\Sale\OrderTable, Bitrix\Main\UserTable, таблицы highload-блоков, инфоблоков через ElementTable и SectionTable, и любые кастомные сущности модулей.
Главное практическое отличие: D7 сам следит за типами полей, экранирует значения в фильтре и строит JOIN через объявленные связи, а не через ручную склейку SQL. На проекте с каталогом в 15-20 тысяч товаров переход на D7 в отчётах по остаткам избавил от десятка мест, где раньше значения подставлялись в SQL напрямую, включая пользовательский ввод из фильтра в админке.
Базовый вызов выглядит так:
use Bitrix\Main\Loader;
use Bitrix\Sale\OrderTable;
Loader::includeModule('sale');
$result = OrderTable::getList([
'select' => ['ID', 'ACCOUNT_NUMBER', 'PRICE', 'DATE_INSERT'],
'filter' => ['=STATUS_ID' => 'F'],
'order' => ['DATE_INSERT' => 'DESC'],
'limit' => 20,
]);
$orders = $result->fetchAll();
fetchAll() возвращает массив ассоциативных массивов, fetch() отдаёт по одной строке, а fetchCollection() строит коллекцию объектов сущности, если она описана как EO-класс. Для отчётов и выгрузок обычно хватает fetchAll, для бизнес-логики с сохранением изменений удобнее объектный вариант.
Метод getList: базовый синтаксис выборки D7 orm
У getList есть фиксированный набор ключей: select, filter, order, group, limit, offset, runtime, cache, count_total. Пропущенный select вытянет вообще все поля таблицы, что на широких сущностях вроде заказов или пользователей ощутимо бьёт по памяти и скорости, поэтому список полей я указываю почти всегда явно, кроме отладочных выборок.
Пагинация делается через limit и offset, а для подсчёта общего числа строк без второго запроса есть флаг count_total:
$result = OrderTable::getList([
'select' => ['ID', 'PRICE'],
'filter' => ['=STATUS_ID' => 'F'],
'limit' => 20,
'offset' => 40,
'count_total' => true,
]);
$rows = $result->fetchAll();
$total = $result->getCount();
Это удобнее, чем делать отдельный getCount запрос вручную: Битрикс сам оборачивает подсчёт в SELECT COUNT(*) с тем же фильтром, без сортировки и лимита.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Фильтры по дате: операторы и типы данных
Самая частая ошибка новичков в D7 - передавать дату строкой. Фильтр по дате должен получать объект Bitrix\Main\Type\DateTime, а не «01.09.2026» текстом: без обёртки сравнение уходит в строковое сопоставление, и результат зависит от формата даты в настройках сайта, а не от реального порядка дат.
| Оператор | Значение | Пример ключа фильтра |
|---|---|---|
| = | равно | ‘=STATUS_ID’ |
| != | не равно | ‘!=STATUS_ID’ |
| >, >= | больше, больше или равно | ‘>=DATE_INSERT’ |
| <, <= | меньше, меньше или равно | ‘<DATE_INSERT’ |
| >< | значение в диапазоне (между двумя границами) | ‘><DATE_INSERT’ => [$from, $to] |
| % | LIKE, поиск по подстроке | ‘%NAME’ |
| @ | IN, список значений массивом | ‘@STATUS_ID’ => [‘N’, ‘P’] |
Выборка заказов за конкретные сутки на практике выглядит так:
use Bitrix\Main\Type\DateTime;
$from = new DateTime('01.09.2026 00:00:00');
$to = new DateTime('02.09.2026 00:00:00');
$result = OrderTable::getList([
'select' => ['ID', 'ACCOUNT_NUMBER', 'DATE_INSERT'],
'filter' => [
'>=DATE_INSERT' => $from,
'<DATE_INSERT' => $to,
],
'order' => ['DATE_INSERT' => 'ASC'],
]);
Я намеренно беру строгое «меньше» для верхней границы, а не «меньше или равно» с концом дня: так не проваливается заказ, оформленный ровно в полночь следующих суток. На одном проекте с доставкой через СДЭК похожая выборка формировала список заказов, которые нужно передать в СДЭК на завтрашний рабочий день: фильтр по DATE_INSERT и статусу заказа, без единой строки сырого SQL.
Есть нюанс с таймзоной: DateTime в D7 берёт таймзону сайта из настроек, а не таймзону сервера. Если крон-скрипт или консольный обработчик запускается вне контекста сайта, время может уехать на несколько часов - в таких местах я явно указываю таймзону через DateTimeZone при создании объекта.
Сортировка результатов через order
order принимает массив «поле => направление», порядок ключей в массиве важен и определяет порядок в ORDER BY:
$result = OrderTable::getList([
'select' => ['ID', 'PRICE', 'DATE_INSERT'],
'order' => [
'PRICE' => 'DESC',
'DATE_INSERT' => 'ASC',
],
]);
Сортировать можно и по полю связанной таблицы, но только если это поле уже участвует в select через алиас связи, например USER.NAME. Без этого ORM выбросит исключение о несуществующем поле, потому что для сортировки по чужой таблице нужен реальный JOIN, а не просто упоминание поля.
Отдельная тонкость - сортировка с учётом NULL. Штатных NULLS FIRST и NULLS LAST в D7 нет, приходится либо сортировать в PHP после выборки, либо городить runtime-поле с выражением через IFNULL. На небольших выборках (до нескольких тысяч строк) проще досортировать в PHP - меньше кода и нагрузки на MySQL.
Связи между таблицами: reference и runtime-поля
Связи в D7 бывают двух видов: постоянные, описанные в классе сущности, и разовые, объявленные прямо в вызове getList через runtime.
Постоянная связь задаётся в методе getMap кастомного класса-наследника DataManager:
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\UserTable;
class OrderExtTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName()
{
return 'b_sale_order';
}
public static function getMap()
{
return [
'ID' => ['data_type' => 'integer', 'primary' => true],
'USER_ID' => ['data_type' => 'integer'],
'USER' => new ReferenceField(
'USER',
UserTable::class,
Join::on('this.USER_ID', 'ref.ID')
),
];
}
}
После такого объявления связь используется как обычное поле через точку:
$rows = OrderExtTable::getList([
'select' => ['ID', 'USER_NAME' => 'USER.NAME', 'USER_EMAIL' => 'USER.EMAIL'],
'filter' => ['=USER.ACTIVE' => 'Y'],
])->fetchAll();
Если лезть в код модуля ради одной связи не хочется, можно объявить её прямо в запросе через runtime - удобно для разовых отчётов или интеграций, которые живут в отдельном локальном модуле:
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\UserTable;
$rows = OrderTable::getList([
'runtime' => [
new ReferenceField('USER', UserTable::class, Join::on('this.USER_ID', 'ref.ID')),
],
'select' => ['ID', 'USER_NAME' => 'USER.NAME'],
'filter' => ['=USER.ACTIVE' => 'Y'],
])->fetchAll();
Этот же приём работает для связи highload-блоков с элементами инфоблока: чаще всего свойство хранит ID записи HL-справочника, и через runtime с ReferenceField можно вытянуть название напрямую в выборке, а не подгружать справочник отдельным запросом на каждую строку в цикле. Если нужна доработка типовых выборок под нестандартный каталог или интеграция с внешней CRM, обычно быстрее заказать это у стороннего разработчика - можно обратиться за поддержкой и сопровождением сайтов.
Оптимизация: select, кэш и проблема N+1
Три вещи, которые реально влияют на скорость выборок D7 orm на боевых проектах:
- Явный
selectс нужными полями вместо выгрузки всей строки таблицы. - Кэш выборки через ключ
cache, если данные не меняются на каждый чих:'cache' => ['ttl' => 3600, 'cache_joins' => true]. - Один запрос с JOIN вместо цикла с отдельным запросом на каждую строку, он же N+1.
Проблема N+1 в D7 выглядит невинно: сначала getList для заказов, потом внутри foreach ещё один getList для пользователя каждого заказа. На выборке в 20 строк разницы не видно, на отчёте за месяц с парой тысяч заказов это превращается в пару тысяч лишних запросов к MySQL и заметную просадку страницы админки. Решение то же, что и в разделе про связи: заводить ReferenceField и тянуть нужные поля сразу в основном select, а не достраивать их после.
Частые ошибки при работе с D7 ORM выборками
- Дата передаётся строкой вместо объекта
DateTime- фильтр молча сравнивает строки не в том порядке. - Сортировка или фильтр по полю связанной таблицы без объявленной связи - ORM выбрасывает исключение вместо того, чтобы достроить JOIN сам.
- Сырой SQL-фрагмент, вставленный в фильтр через
Bitrix\Main\ORM\Query\Query::exprс пользовательским вводом без экранирования - редкий, но реальный риск SQL-инъекции, если данные пришли из формы. - Отсутствие
limitна выборках с потенциально большим числом строк - на инфоблоке с десятками тысяч элементов это ощутимо нагружает и MySQL, и PHP-процесс, который держит весь результат в памяти. - N+1 запросы внутри цикла вместо join через
runtimeили постоянныйReferenceField.
Большая часть этих ошибок не ловится тестами и всплывает только под реальной нагрузкой, когда выборка перестаёт укладываться в разумное время ответа.
Частые вопросы
Чем D7 ORM отличается от старого API вроде CIBlockElement::GetList?
D7 строит запрос через объявленные поля и связи сущности, а не через ручную склейку SQL-условий. Значения фильтра экранируются автоматически, типы данных проверяются на этапе объявления карты полей, а результат можно получать как массив или как объект сущности через fetchCollection.
Как отфильтровать записи по диапазону дат в D7?
Диапазон делается либо оператором >< с массивом из двух значений, либо парой условий >= и < на одно и то же поле. Обе границы должны быть объектами Bitrix\Main\Type\DateTime, а не строками, иначе сравнение уйдёт в текстовое сопоставление вместо числового.
Можно ли сделать JOIN без создания отдельного класса сущности?
Да, через ключ runtime в вызове getList: там можно объявить ReferenceField прямо в запросе, без правки карты полей исходного класса. Такой подход удобен для одноразовых отчётов и интеграций из локального модуля.
Почему сортировка по полю связанной таблицы выдаёт ошибку?
D7 не строит JOIN автоматически ради сортировки. Поле связанной таблицы нужно сначала явно объявить через select или runtime, и только после этого им можно пользоваться в order и filter.