Главная/Статьи/ORM в Битрикс D7 — сущности, связи и кастомные таблицы

ORM в Битрикс D7 — сущности, связи и кастомные таблицы

Полный гайд по ORM D7: создание своих сущностей, связи между таблицами, валидация, события. Уходим от CIBlockElement::GetList к современному API.

ДМ
Дмитрий Мещеряков
📅 21 августа 2026 г.📖 8 мин чтения

ORM в Битриксе появилась в D7 и с тех пор живёт рядом со старым API, а не вместо него. Это порождает главный вопрос при работе с ней: не «как написать getList», а «где проходит граница» — какие сущности можно трогать через ORM, а какие только через старые классы, потому что за ними стоит логика, которой в ORM нет.

Разберём и то, и другое: базовые операции, свои сущности со связями — и места, где переход на ORM ломает работающий код.

Зачем ORM в Битрикс

Старое API (CIBlockElement::GetList) — это массивы, магические ключи и неочевидное поведение. ORM D7 даёт:

  • Типизированные сущности
  • Автодополнение в IDE
  • Валидацию на уровне модели
  • Связи между таблицами
  • Единый интерфейс для всех данных
💡 Совет

ORM D7 — не замена инфоблокам, а способ работы с любыми таблицами MySQL через единый API. Для инфоблоков есть \Bitrix\Iblock\Elements\ElementXxxTable.

Базовая работа с существующими сущностями

Чтение данных

php
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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);
    }
}

Итоги

ЗадачаСтарый APIORM D7
ВыборкаGetList() с массивамиTable::getList() с типами
ДобавлениеAdd() без валидацииTable::add() с Result
СвязиРучные JOINReference / 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.