1С Битрикс · 7 мин чтения

D7 ORM выборки в Битрикс: фильтры, сортировка и связи

Когда нужно вытащить из базы список заказов за период, отсортировать товары по остаткам или подтянуть данные из связанной таблицы, встаёт выбор: писать 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.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно