ORM в Битриксе появилась в D7 и с тех пор живёт рядом со старым API, а не вместо него. Это порождает главный вопрос при работе с ней: не «как написать getList», а «где проходит граница» — какие сущности можно трогать через ORM, а какие только через старые классы, потому что за ними стоит логика, которой в ORM нет.
Разберём и то, и другое: базовые операции, свои сущности со связями — и места, где переход на ORM ломает работающий код.
Зачем ORM в Битрикс
Старое API (CIBlockElement::GetList) — это массивы, магические ключи и неочевидное поведение. ORM D7 даёт:
- Типизированные сущности
- Автодополнение в IDE
- Валидацию на уровне модели
- Связи между таблицами
- Единый интерфейс для всех данных
ORM D7 — не замена инфоблокам, а способ работы с любыми таблицами MySQL через единый API. Для инфоблоков есть \Bitrix\Iblock\Elements\ElementXxxTable.
Базовая работа с существующими сущностями
Чтение данных
<?php
use Bitrix\Main\UserTable;
// Получить одного пользователя
$user = UserTable::getById(1)->fetch();
// Список с фильтром
$users = UserTable::getList([
'select' => ['ID', 'LOGIN', 'EMAIL', 'NAME'],
'filter' => [
'ACTIVE' => 'Y',
'>=DATE_REGISTER' => new \Bitrix\Main\Type\DateTime('2026-01-01'),
],
'order' => ['DATE_REGISTER' => 'DESC'],
'limit' => 10,
])->fetchAll();
// Подсчёт
$count = UserTable::getCount(['ACTIVE' => 'Y']);Создание и обновление
<?php
use Bitrix\Main\UserTable;
// Создание
$result = UserTable::add([
'LOGIN' => 'newuser',
'EMAIL' => 'user@example.com',
'PASSWORD' => 'secret123',
'NAME' => 'Иван',
'LAST_NAME' => 'Петров',
]);
if ($result->isSuccess()) {
$userId = $result->getId();
} else {
$errors = $result->getErrorMessages();
}
// Обновление
$result = UserTable::update($userId, [
'NAME' => 'Пётр',
]);
// Удаление
$result = UserTable::delete($userId);Общее правило, к которому это сводится. ORM — прямой доступ к таблице. Она хороша ровно там, где за таблицей нет ничего, кроме таблицы: ваши собственные сущности, служебные справочники, логи. Для сущностей ядра — пользователей, заказов, товаров, элементов инфоблоков — существует прикладной слой с валидацией и событиями, и обход этого слоя даёт не «быстрее», а «данные в базе есть, а система про них не знает».
Создание своей сущности
Структура файла
<?php
// /local/lib/Entity/OrderTable.php
namespace Local\Entity;
use Bitrix\Main\Entity;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields;
class OrderTable extends DataManager
{
/**
* Имя таблицы в БД
*/
public static function getTableName(): string
{
return 'local_orders';
}
/**
* Описание полей
*/
public static function getMap(): array
{
return [
// Первичный ключ
new Fields\IntegerField('ID', [
'primary' => true,
'autocomplete' => true,
]),
// Обязательное строковое поле
new Fields\StringField('ORDER_NUMBER', [
'required' => true,
'validation' => function () {
return [
new Fields\Validators\LengthValidator(1, 50),
];
},
]),
// Внешний ключ
new Fields\IntegerField('USER_ID', [
'required' => true,
]),
// Enum поле
new Fields\EnumField('STATUS', [
'required' => true,
'values' => ['new', 'processing', 'completed', 'cancelled'],
'default_value' => 'new',
]),
// Денежное поле
new Fields\FloatField('TOTAL', [
'required' => true,
'default_value' => 0.00,
]),
// JSON данные
new Fields\TextField('ITEMS_JSON'),
// Дата создания
new Fields\DatetimeField('CREATED_AT', [
'required' => true,
'default_value' => function () {
return new \Bitrix\Main\Type\DateTime();
},
]),
// Дата обновления
new Fields\DatetimeField('UPDATED_AT'),
];
}
}Создание таблицы
<?php
// /local/php_interface/migrations/install_orders.php
use Bitrix\Main\Application;
$connection = Application::getConnection();
$connection->queryExecute("
CREATE TABLE IF NOT EXISTS local_orders (
ID INT(11) NOT NULL AUTO_INCREMENT,
ORDER_NUMBER VARCHAR(50) NOT NULL,
USER_ID INT(11) NOT NULL,
STATUS ENUM('new', 'processing', 'completed', 'cancelled') DEFAULT 'new',
TOTAL DECIMAL(15, 2) DEFAULT 0.00,
ITEMS_JSON TEXT,
CREATED_AT DATETIME NOT NULL,
UPDATED_AT DATETIME,
PRIMARY KEY (ID),
INDEX idx_user (USER_ID),
INDEX idx_status (STATUS),
INDEX idx_created (CREATED_AT)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");Связи между сущностями
Reference (один к одному / многие к одному)
<?php
namespace Local\Entity;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields;
class OrderTable extends DataManager
{
public static function getMap(): array
{
return [
// ... остальные поля
// Связь с пользователем
new Fields\Relations\Reference(
'USER',
\Bitrix\Main\UserTable::class,
['=this.USER_ID' => 'ref.ID'],
['join_type' => 'LEFT']
),
];
}
}
// Использование
$orders = OrderTable::getList([
'select' => [
'ID',
'ORDER_NUMBER',
'TOTAL',
'USER.LOGIN', // Поле из связанной таблицы
'USER.EMAIL',
],
'filter' => ['USER.ACTIVE' => 'Y'],
])->fetchAll();OneToMany (один ко многим)
<?php
// OrderItemTable.php
namespace Local\Entity;
class OrderItemTable extends DataManager
{
public static function getTableName(): string
{
return 'local_order_items';
}
public static function getMap(): array
{
return [
new Fields\IntegerField('ID', [
'primary' => true,
'autocomplete' => true,
]),
new Fields\IntegerField('ORDER_ID', ['required' => true]),
new Fields\StringField('PRODUCT_NAME', ['required' => true]),
new Fields\IntegerField('QUANTITY', ['default_value' => 1]),
new Fields\FloatField('PRICE'),
// Связь с заказом
new Fields\Relations\Reference(
'ORDER',
OrderTable::class,
['=this.ORDER_ID' => 'ref.ID']
),
];
}
}
// В OrderTable добавляем обратную связь
new Fields\Relations\OneToMany(
'ITEMS',
OrderItemTable::class,
'ORDER'
),
// Использование
$order = OrderTable::getByPrimary(123, [
'select' => ['*', 'ITEMS'],
])->fetchObject();
foreach ($order->getItems() as $item) {
echo $item->getProductName();
}Валидация
<?php
use Bitrix\Main\ORM\Fields;
use Bitrix\Main\ORM\Fields\Validators;
class OrderTable extends DataManager
{
public static function getMap(): array
{
return [
new Fields\StringField('EMAIL', [
'validation' => function () {
return [
new Validators\EmailValidator(),
];
},
]),
new Fields\StringField('PHONE', [
'validation' => function () {
return [
new Validators\RegExpValidator('/^\+7\d{10}$/'),
];
},
]),
new Fields\FloatField('TOTAL', [
'validation' => function () {
return [
function ($value) {
if ($value < 0) {
return 'Сумма не может быть отрицательной';
}
return true;
},
];
},
]),
];
}
}События сущности
<?php
class OrderTable extends DataManager
{
// Перед добавлением
public static function onBeforeAdd(Entity\Event $event)
{
$fields = $event->getParameter('fields');
$result = new Entity\EventResult();
// Генерируем номер заказа
if (empty($fields['ORDER_NUMBER'])) {
$result->modifyFields([
'ORDER_NUMBER' => 'ORD-' . time() . '-' . random_int(1000, 9999),
]);
}
return $result;
}
// После добавления
public static function onAfterAdd(Entity\Event $event)
{
$id = $event->getParameter('id');
$fields = $event->getParameter('fields');
// Отправляем уведомление
self::sendNotification($id, 'created');
}
// Перед обновлением
public static function onBeforeUpdate(Entity\Event $event)
{
$result = new Entity\EventResult();
$result->modifyFields([
'UPDATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
return $result;
}
// После удаления
public static function onAfterDelete(Entity\Event $event)
{
$id = $event->getParameter('id');
// Удаляем связанные записи
OrderItemTable::deleteByFilter(['=ORDER_ID' => $id]);
}
}Работа с объектами (EO_*)
<?php
// Создание объекта
$order = OrderTable::createObject();
$order->setOrderNumber('ORD-001');
$order->setUserId(1);
$order->setTotal(5000.00);
$order->save();
// Получение объекта
$order = OrderTable::getByPrimary(123)->fetchObject();
// Изменение
$order->setStatus('completed');
$order->save();
// Коллекции
$orders = OrderTable::getList([
'filter' => ['USER_ID' => 1],
])->fetchCollection();
foreach ($orders as $order) {
echo $order->getOrderNumber() . ': ' . $order->getTotal();
}
// Массовое обновление через коллекцию
$orders->fill(['STATUS' => 'cancelled']);
$orders->save();Практический пример: репозиторий
<?php
namespace Local\Repository;
use Local\Entity\OrderTable;
use Bitrix\Main\Type\DateTime;
class OrderRepository
{
public function findById(int $id): ?array
{
return OrderTable::getById($id)->fetch() ?: null;
}
public function findByUser(int $userId, int $limit = 10): array
{
return OrderTable::getList([
'select' => ['*', 'ITEMS'],
'filter' => ['USER_ID' => $userId],
'order' => ['CREATED_AT' => 'DESC'],
'limit' => $limit,
])->fetchAll();
}
public function findPending(): array
{
return OrderTable::getList([
'filter' => [
'STATUS' => 'new',
'<CREATED_AT' => (new DateTime())->add('-1 day'),
],
])->fetchAll();
}
public function create(array $data): int
{
$result = OrderTable::add($data);
if (!$result->isSuccess()) {
throw new \RuntimeException(
implode(', ', $result->getErrorMessages())
);
}
return $result->getId();
}
public function updateStatus(int $id, string $status): bool
{
$result = OrderTable::update($id, ['STATUS' => $status]);
return $result->isSuccess();
}
public function getTotalByPeriod(DateTime $from, DateTime $to): float
{
$row = OrderTable::getList([
'select' => [
new \Bitrix\Main\Entity\ExpressionField('SUM_TOTAL', 'SUM(%s)', ['TOTAL']),
],
'filter' => [
'>=CREATED_AT' => $from,
'<=CREATED_AT' => $to,
'STATUS' => 'completed',
],
])->fetch();
return (float) ($row['SUM_TOTAL'] ?? 0);
}
}Итоги
| Задача | Старый API | ORM D7 |
|---|---|---|
| Выборка | GetList() с массивами | Table::getList() с типами |
| Добавление | Add() без валидации | Table::add() с Result |
| Связи | Ручные JOIN | Reference / OneToMany |
| Валидация | В коде бизнес-логики | В описании сущности |
| События | Разрозненные | В классе сущности |
Правила, которые я применяю на практике.
ORM — для своих таблиц, прикладное API — для сущностей ядра. Это главное. Пользователи через CUser, заказы через модуль sale, элементы инфоблоков через CIBlockElement (или ORM-обёртки инфоблоков, если они вам подходят). Своя таблица заказов интеграции, лог обмена, справочник — через ORM.
Совет «не смешивайте старый и новый API» практически невыполним. В реальном проекте вы будете писать и то, и другое, причём иногда в одном методе. Полезная формулировка другая: не смешивайте их в рамках одной операции над одной сущностью. Прочитать элемент через ORM, а сохранить через CIBlockElement::Update() — нормально. Обновить часть полей заказа через ORM, а часть через API sale в одной транзакции — источник трудноуловимых расхождений, потому что вторая половина не увидит первую.
select пишите явно. Без него ORM тянет все скалярные поля, а на связанных сущностях легко получить лишние запросы. Если в выборке участвует Reference, поле из связанной сущности должно быть перечислено в select — иначе оно догрузится отдельным запросом на каждую строку.
limit — всегда. ORM без ограничения с удовольствием вернёт вам всю таблицу в память.
Проверяйте Result. add() и update() не бросают исключений при бизнес-ошибках: они возвращают объект, и isSuccess() нужно смотреть. Пропущенная проверка — это «данные не сохранились, ошибок нет».
Про ExpressionField. В примерах встречается \Bitrix\Main\Entity\ExpressionField — старое пространство имён, оно ещё работает, но актуальное сейчас \Bitrix\Main\ORM\Fields\ExpressionField. То же касается Entity\DataManager и других классов из Bitrix\Main\Entity: в новом коде берите ветку Bitrix\Main\ORM.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.