СезонГотовим магазин к высокому сезону и Чёрной пятнице: скорость, нагрузка, акции

D7 и ORM в 1С-Битрикс: современная разработка вместо старого API

D7 и ORM в 1С-Битрикс: современная разработка, пространства имён, DataManager, getList и связи вместо старого API

Разработчик открывает старый проект на 1С-Битрикс и видит знакомую картину: CIBlockElement::GetList с массивами фильтров, $arSelect со строковыми ключами, CModule::IncludeModule в начале каждого файла и глобальные объекты вроде $USER и $APPLICATION. Всё работает, но код нечитаемый, ошибки в фильтрах всплывают только в рантайме, а IDE не подсказывает ни методов, ни полей. Рядом в той же системе живёт другой мир — ядро D7: пространства имён Bitrix\Main, автозагрузка классов, ORM с DataManager и типизированными выборками. Вопрос не в том, «работает ли старое API» (работает), а в том, как писать новое, чтобы проект было приятно поддерживать и расширять.

Эта статья — практическая карта D7 и ORM для тех, кто пишет на Битриксе и хочет уйти от «магических массивов» старого ядра к современному коду. Разберём, что такое ядро D7 в отличие от старого API, как устроены пространства имён и автозагрузка, что делают Application, Context и сервис-локатор, как работает ORM — Entity, DataManager, методы add/update/delete/getList, поля ScalarField и Reference, связи и join, рантайм-поля и агрегаты, кэш выборок и транзакции, а также HighloadBlock через ORM. Покажем, чем D7 объективно лучше CIBlockElement::GetList и CModule, как совмещать старое и новое и как аккуратно мигрировать. Материал — выжимка из наших highload-проектов на 1С-Битрикс, где цена читаемости и скорости кода особенно высока.

Коротко

  • D7 — современное объектное ядро Битрикса: пространства имён (Bitrix\Main, Bitrix\Iblock), автозагрузка классов и ORM вместо процедурного старого API.
  • ORM строится на DataManager: класс описывает таблицу и поля, а add/update/delete/getList дают типизированную и безопасную работу с данными.
  • getList d7 принимает понятную структуру select/filter/order/runtime; связи через Reference и join, агрегаты — через рантайм-поля.
  • D7 лучше CIBlockElement::GetList по типизации, читаемости и безопасности; но старое и новое ядро сосуществуют — их можно совмещать.
  • HighloadBlock изначально на ORM: своя таблица плюс DataManager — идеально для больших справочников. Миграцию делают модуль за модулем, без «переписать всё сразу».

Зачем разработчику переходить на D7

1С-Битрикс тащит на себе тяжёлое наследие: платформа развивается с начала 2000-х, и старое процедурное API — часть её ДНК. Оно никуда не денется, потому что на нём написаны миллионы строк рабочего кода. Но писать новую логику в старом стиле в 2020-х — значит сознательно усложнять себе жизнь. D7 существует именно для того, чтобы новый код был чище, безопаснее и быстрее в разработке.

Переход на D7 даёт три практических выигрыша, которые чувствуются на каждом проекте:

Обратная сторона — стоимость незнания. Проекты, где всё написано «по-старому», со временем становится всё труднее развивать: логика размазана по глобальным функциям, тесты писать невозможно, а любая правда о данных достаётся только через отладочный print_r. D7 не волшебная таблетка, но это тот фундамент, на котором современный код Битрикса держится десятилетие. Разберёмся, что это за фундамент.

Что такое ядро D7 и как оно появилось

«D7» — это сокращение от «ядро версии D7» (по внутренней нумерации платформы). Исторически у Битрикса было старое ядро — набор глобальных классов с префиксом C (CIBlockElement, CUser, CModule, CSaleOrder) и функций, живущих в глобальном пространстве. Оно процедурно-объектное: классы есть, но без пространств имён, без автозагрузки, с массивами вместо типизированных структур. Это работало годами, но плохо масштабировалось под современные требования к архитектуре.

D7 — это переписанное на нормальном ООП ядро, которое ввели, чтобы платформа говорила на языке современного PHP: пространства имён, автозагрузка по стандарту, статические фабрики, ORM, событийная модель нового образца. Ключевая мысль: D7 не отдельный продукт и не «новый Битрикс» — это второй, современный слой того же самого ядра. Он живёт в тех же модулях, что и старое API, просто в другом пространстве имён.

Практически это выглядит так. Функциональность модуля main в старом стиле — это CUser, CFile, CSite; в D7 — пространство имён Bitrix\Main с классами Bitrix\Main\UserTable, Bitrix\Main\Application, Bitrix\Main\Type\DateTime. Функциональность модуля инфоблоков в старом стиле — CIBlockElement, CIBlockSection; в D7 — Bitrix\Iblock\ElementTable, Bitrix\Iblock\SectionTable и объектный API инфоблоков. Оба набора доступны в любой точке кода после подключения модуля.

Важно не путать: «D7» — это про ядро и стиль кода, а не про версию продукта, которую вы купили. Даже на давно работающем магазине D7 уже доступен — им можно пользоваться прямо сейчас, ничего не «обновляя». Вопрос лишь в том, каким API вы пишете новую логику: старыми C-классами или пространствами имён Bitrix\*.

Старое ядро и D7: два слоя одной платформы

Чтобы уверенно писать на Битриксе, нужно держать в голове картину двух слоёв. Снизу — общая инфраструктура: база данных, файловая система, модули. Над ней — два параллельных API к одной и той же функциональности: старое процедурно-объектное (C-классы и функции) и современное D7 (пространства имён и ORM). Они не конкурируют за данные — работают с одними и теми же таблицами, просто по-разному их выражают.

Это объясняет, почему миграция на D7 не требует «всё сломать и переписать». Вы можете в одном контроллере выбрать заказы через Bitrix\Sale\Order и тут же обратиться к старому CSaleBasket, если так проще. Данные согласованы, потому что слой хранения общий. Разница только в удобстве и безопасности того кода, который пишете вы.

Ниже — принципиальная схема соотношения слоёв. Она хорошо укладывает в голову главную мысль: D7 не «поверх» и не «вместо» старого ядра, а рядом, как второй фасад к общей инфраструктуре.

Два API к одной платформе: старое ядро и D7 Старое ядро (legacy) CIBlockElement, CUser CModule, CSaleOrder массивы, глобальные функции Ядро D7 (современное) Bitrix\Main, Bitrix\Iblock ORM, DataManager классы, автозагрузка, типизация Общая инфраструктура модули, события, кэш, права доступа одна база данных / одни таблицы
Схема: старое ядро и D7 — два параллельных API к общей инфраструктуре и единой базе данных. Новый код пишут на D7, но старое API остаётся доступным в той же системе.

Пространства имён и автозагрузка классов

Первое, что отличает код D7 внешне, — обращения к классам вида \Bitrix\Main\Type\DateTime вместо CIBlockElement. Это пространства имён (namespaces). Они решают две задачи: устраняют конфликты имён (в модулях main и sale могут быть классы с одинаковым коротким именем) и делают путь к исходнику предсказуемым — по полному имени класса сразу понятно, из какого модуля он и где лежит файл.

Вторая половина картины — автозагрузка. В старом коде классы подключались вручную: чтобы использовать функциональность модуля, писали CModule::IncludeModule('iblock'), а иногда и прямые require. В D7 классы загружаются сами: когда PHP впервые встречает обращение к \Bitrix\Iblock\ElementTable, автозагрузчик Битрикса по namespace вычисляет путь к файлу и подключает его. Никаких ручных require для ядра.

На практике типичный «пролог» файла D7 выглядит так — подключаем модуль (для регистрации его автозагрузки) и объявляем алиасы use для коротких имён:

<?php
use Bitrix\Main\Loader;
use Bitrix\Iblock\ElementTable;
use Bitrix\Main\Type\DateTime;

// Регистрируем модуль (и его автозагрузку классов) — современный аналог CModule
if (!Loader::includeModule('iblock')) {
    throw new \RuntimeException('Модуль iblock не установлен');
}

// Дальше классы модуля доступны без единого require:
$now = new DateTime();                 // \Bitrix\Main\Type\DateTime
$rows = ElementTable::getList([...]);   // \Bitrix\Iblock\ElementTable

Обратите внимание на Loader::includeModule() — это D7-замена CModule::IncludeModule(). Она возвращает bool, и хорошая привычка — проверять результат, а не вызывать «на удачу». Для своих классов автозагрузку регистрируют через описание в собственном модуле или через Loader::registerAutoLoadClasses(), указывая соответствие имени класса и файла. Тогда ваш \Vendor\Shop\PriceCalculator будет подгружаться так же автоматически, как классы ядра.

Ниже — как namespace превращается в путь к файлу. Именно эта однозначность и делает автозагрузку возможной.

Автозагрузка: namespace → файл Обращение к классу \Bitrix\Iblock\ ElementTable Автозагрузчик разбирает namespace и ищет файл модуля Подключение файла /bitrix/modules/iblock/ lib/elementtable.php Ни одного ручного require полное имя класса однозначно указывает на файл — загрузка происходит сама
Схема: полное имя класса с пространством имён однозначно отображается в путь к файлу модуля, и автозагрузчик подключает его без ручных require — отсюда предсказуемость и отсутствие конфликтов имён.

Application, Context и сервис-локатор

В старом коде «точки входа» в систему — глобальные объекты: $APPLICATION, $USER, $DB. Они удобны, но это глобальное состояние, которое трудно тестировать и легко случайно сломать. В D7 те же сущности доступны через объект приложения — \Bitrix\Main\Application, — который выступает единой точкой доступа к сервисам системы.

Ключевые вещи, которые дают Application и связанный с ним Context:

<?php
use Bitrix\Main\Application;

// Соединение с базой из пула — для транзакций и низкоуровневых операций
$connection = Application::getConnection();

// Контекст запроса вместо прямого доступа к суперглобальным массивам
$request = Application::getInstance()->getContext()->getRequest();
$page = (int)($request->get('PAGEN_1') ?? 1);   // безопаснее, чем $_GET['PAGEN_1']
$isPost = $request->isPost();

В новых версиях ядра к этому добавился сервис-локатор — \Bitrix\Main\DI\ServiceLocator. Идея та же, что в любом контейнере зависимостей: сервисы (объекты, реализующие какую-то задачу) регистрируются под именем, а код получает их по этому имени, не создавая вручную. Это шаг к тому, чтобы бизнес-логику можно было подменять и тестировать, а не «прибивать гвоздями» к глобальным объектам.

Практический смысл: когда видите в чужом D7-коде Application::getConnection() или ServiceLocator::getInstance()->get(...), знайте — это осознанный уход от глобального состояния. Такой код проще переносить, тестировать и читать, потому что зависимости получаются явно, а не берутся «из воздуха» из глобальных переменных.

ORM: Entity, DataManager и таблица

Сердце D7 для повседневной работы — ORM (Object-Relational Mapping), объектное отображение данных. Его задача — связать таблицу в базе с классом в коде так, чтобы вы работали не с сырым SQL и не с безымянными массивами, а с описанной сущностью. Три понятия, которые нужно уложить в голову: таблица, Entity (сущность) и DataManager.

Минимальный класс-наследник DataManager выглядит так — он описывает таблицу и её поля декларативно:

<?php
namespace Vendor\Shop;

use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Fields\DatetimeField;

class OrdersLogTable extends DataManager
{
    // Какую таблицу обслуживает этот класс
    public static function getTableName()
    {
        return 'my_shop_orders_log';
    }

    // Описание полей таблицы — типизированно и явно
    public static function getMap()
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new IntegerField('ORDER_ID', ['required' => true]),
            new StringField('STATUS', ['required' => true]),
            new DatetimeField('CREATED_AT'),
        ];
    }
}

Всё — этого достаточно, чтобы получить полноценный API к таблице: OrdersLogTable::add(), ::update(), ::delete(), ::getList(). Ни строчки SQL, ни ручного экранирования. Метод getMap() — это контракт: он описывает поля, их типы и ограничения (primary, required, значения по умолчанию). ORM по нему знает, как строить запросы и как валидировать данные.

ORM: класс DataManager ↔ таблица базы OrdersLogTable extends DataManager getTableName() → my_shop_orders_log getMap() ID, ORDER_ID, STATUS, CREATED_AT add / update / delete / getList Entity поля, типы, связи Таблица БД ID | ORDER_ID STATUS | CREATED_AT 101 | 5540 | new | ... 102 | 5541 | paid | ... 103 | 5542 | sent | ... строит маппинг
Схема: класс-наследник DataManager через getTableName и getMap описывает сущность Entity, а ORM отображает её на физическую таблицу базы — чтение и запись идут методами класса, без сырого SQL.

DataManager: add, update, delete

Запись данных через ORM — это три метода класса-наследника: add(), update(), delete(). Все они возвращают объект результата (Result), у которого можно спросить, успешна ли операция и какие были ошибки. Это принципиально удобнее старого стиля, где результат — то false, то ID, то предупреждение в глобальном объекте.

Добавление. Передаём массив полей, получаем результат с ID новой записи:

<?php
use Vendor\Shop\OrdersLogTable;
use Bitrix\Main\Type\DateTime;

$result = OrdersLogTable::add([
    'ORDER_ID'   => 5540,
    'STATUS'     => 'new',
    'CREATED_AT' => new DateTime(),
]);

if ($result->isSuccess()) {
    $newId = $result->getId();
} else {
    // Ошибки валидации доступны явно — не нужно ловить их из глобалей
    $errors = $result->getErrorMessages();
}

Обновление. Первым аргументом — первичный ключ, вторым — изменяемые поля:

<?php
$result = OrdersLogTable::update($newId, [
    'STATUS' => 'paid',
]);

if (!$result->isSuccess()) {
    // обработка ошибок
}

Удаление. Достаточно первичного ключа:

<?php
$result = OrdersLogTable::delete($newId);

Обратите внимание на единый паттерн: любая операция возвращает Result, вы проверяете isSuccess() и при необходимости читаете getErrorMessages(). Это делает обработку ошибок явной и одинаковой во всём проекте. Для массовых операций есть свои приёмы (например, updateByFilter и deleteByFilter в поддерживающих их сущностях), но базовый контракт остаётся тем же.

Тонкость: DataManager для своих таблиц пишет прямо в них, без бизнес-логики модуля. Для сущностей ядра (элементы инфоблоков, заказы) писать через «сырой» табличный класс опасно — вы обойдёте события, права и пересчёты. Для таких данных используйте специализированный объектный API (например, объекты заказа в Bitrix\Sale) или, где его ещё нет, старое API. Про это подробнее в разделе о совместимости.

getList в D7: чтение данных

Главный метод чтения в ORM — getList(). Именно он заменяет CIBlockElement::GetList и делает выборки читаемыми. Он принимает один ассоциативный массив параметров с понятными ключами:

КлючНазначение
selectКакие поля выбрать (в том числе через связи и рантаймы)
filterУсловия отбора; операторы задаются в ключе (>=, @, %, !)
orderСортировка: поле => ASC/DESC
limit / offsetОграничение количества и смещение (пагинация)
groupГруппировка для агрегатов
runtimeВременные поля: связи, выражения, агрегаты «на лету»
cacheКэширование выборки (время жизни, кэш join-ов)

Базовая выборка выглядит так — сравните читаемость со старым API:

<?php
use Vendor\Shop\OrdersLogTable;

$rows = OrdersLogTable::getList([
    'select' => ['ID', 'ORDER_ID', 'STATUS', 'CREATED_AT'],
    'filter' => [
        '=STATUS'     => 'paid',   // точное равенство
        '>=CREATED_AT' => '2025-01-01 00:00:00',
    ],
    'order'  => ['CREATED_AT' => 'DESC'],
    'limit'  => 50,
])->fetchAll();

foreach ($rows as $row) {
    // $row — обычный ассоциативный массив выбранных полей
    echo $row['ORDER_ID'] . ' — ' . $row['STATUS'];
}

Несколько важных деталей. Метод возвращает объект-курсор; fetchAll() отдаёт все строки массивом, а fetch() — по одной в цикле (экономнее по памяти на больших выборках). Операторы фильтра задаются префиксом в ключе: = — равно, ! — не равно, >=/<= — сравнение, % — LIKE, @ — IN (значение-массив). Всё это параметризуется автоматически — SQL-инъекции здесь неоткуда взяться, если вы не собираете сырые выражения руками.

Для инфоблоков есть готовые сущности ядра — \Bitrix\Iblock\ElementTable. Через них товары и элементы выбираются тем же getList, и это прямая типизированная замена CIBlockElement::GetList. Как это связано с общей вёрсткой каталога и компонентами, мы разбираем в статье про шаблоны и компоненты 1С-Битрикс — там видно, где заканчивается выборка данных и начинается их вывод.

getList: параметры → безопасный SQL → результат Параметры выборки select: [...] filter: [>=..., @...] order: [...] runtime: Reference runtime: Expression group / limit ORM строит JOIN параметризует значения Результат fetchAll() / fetch() массивы строк поля связей приклеены runtime-поля добавляют связи и агрегаты, не меняя класс сущности
Схема: типизированные параметры getList (select, filter, order, runtime) ORM превращает в один параметризованный SQL с нужными join, а результат возвращает готовыми строками — рантайм-поды добавляют связи и агрегаты без правки класса сущности.

Поля сущности: ScalarField и типы

Описание полей в getMap() — не формальность, а то, что делает ORM типизированной. Все поля наследуют от базового ScalarField (скалярное поле — то, что хранится в столбце) и уточняют тип: целое, строка, дата, булево, число с плавающей точкой, текст. Тип определяет, как значение приводится при чтении и записи и как строится условие в фильтре.

Класс поляТип данныхПример столбца
IntegerFieldЦелое числоID, ORDER_ID, количество
StringFieldСтрокаSTATUS, код, артикул
TextFieldДлинный текстКомментарий, описание
BooleanFieldЛогическое (Y/N, 1/0)ACTIVE, IS_PAID
FloatFieldЧисло с точкойЦена, вес, коэффициент
DatetimeField / DateFieldДата и время / датаCREATED_AT, DATE
EnumFieldПеречисление допустимых значенийОграниченный статус
ReferenceFieldСвязь с другой сущностьюСвязь заказа с пользователем
ExpressionFieldВычисляемое выражение / агрегатSUM, COUNT, CONCAT

У каждого поля есть параметры: primary (первичный ключ), autocomplete (автоинкремент), required (обязательное), default_value (значение по умолчанию), values (для булевых — пара значений Y/N). Есть и валидаторы — например, ограничение длины строки — которые проверяют данные до записи и возвращают ошибку в Result, если значение не проходит. Это ещё один уровень защиты: некорректные данные не долетают до базы.

<?php
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Fields\Validators\LengthValidator;

new StringField('STATUS', [
    'required'   => true,
    'validation' => function () {
        return [ new LengthValidator(null, 32) ]; // не длиннее 32 символов
    },
]);

Такое явное описание — то, чего принципиально не хватает старому API: там структура данных нигде не объявлена, и о полях узнаёшь только из документации или отладки. В D7 getMap() — это самодокументируемый контракт таблицы.

Связи и Reference: join между таблицами

Настоящая сила ORM раскрывается на связях. В старом API, чтобы получить к товарам данные из другой таблицы, приходилось делать вложенные выборки в цикле (проблема N+1 запросов) или писать сырой SQL с JOIN вручную. В D7 связь описывается один раз через ReferenceField, и дальше данные связанной сущности «приклеиваются» к выборке одним getList.

Связь объявляют в getMap(): указывают имя связи, класс связанной сущности и условие соединения (ON) через Join::on():

<?php
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\UserTable;

// В getMap() сущности заказов — связь с пользователем по USER_ID
new Reference(
    'USER',                                  // имя связи
    UserTable::class,                        // связанная сущность
    Join::on('this.USER_ID', 'ref.ID')      // условие соединения
);

После этого в select и filter можно обращаться к полям связанной сущности через точку — ORM сама построит нужный JOIN:

<?php
$rows = OrdersTable::getList([
    'select' => [
        'ID',
        'PRICE',
        'USER_NAME'  => 'USER.NAME',    // поле из связанной сущности
        'USER_EMAIL' => 'USER.EMAIL',
    ],
    'filter' => [
        '=USER.ACTIVE' => 'Y',          // фильтр по связанной таблице
        '>PRICE'       => 0,
    ],
    'order'  => ['ID' => 'DESC'],
])->fetchAll();

Одним запросом мы получили заказы вместе с именем и e-mail пользователя, отфильтровав по активности пользователя. Никаких вложенных циклов, никакого ручного SQL, и проблема N+1 решена структурно. Тип связи по умолчанию — LEFT JOIN (при выборе поля связи), но при использовании связи в фильтре ORM применяет INNER JOIN; поведением можно управлять параметром join_type.

Почему это важно для скорости: N+1 — одна из самых частых причин медленных страниц на Битриксе. Когда для списка из 50 товаров код делает 50 дополнительных выборок «за деталями», база захлёбывается. Связи ORM собирают всё в один запрос. Это тесно связано и с кэшированием — как выборки взаимодействуют с кэшем, мы разбираем в статье про кэширование в 1С-Битрикс.

Runtime-поля и агрегаты

Не каждую связь или расчёт имеет смысл «зашивать» в класс сущности навсегда. Для разовых, отчётных или контекстных нужд в D7 есть runtime-поля — виртуальные поля, которые вы добавляете прямо в вызов getList через параметр runtime. Они существуют только в рамках этой выборки.

Runtime-поля бывают трёх видов, и все три невероятно полезны:

Классический пример — посчитать количество и сумму заказов по каждому пользователю. Это агрегат с группировкой, и в ORM он собирается декларативно:

<?php
use Bitrix\Main\ORM\Fields\ExpressionField;

$stats = OrdersTable::getList([
    'select' => [
        'USER_ID',
        'CNT'   => new ExpressionField('CNT', 'COUNT(*)'),
        'TOTAL' => new ExpressionField('TOTAL', 'SUM(%s)', ['PRICE']),
    ],
    'filter' => ['=STATUS' => 'paid'],
    'group'  => ['USER_ID'],
    'order'  => ['TOTAL' => 'DESC'],
])->fetchAll();

// Каждая строка: USER_ID, CNT (число заказов), TOTAL (сумма)

Обратите внимание на плейсхолдер %s в ExpressionField — он безопасно подставляет имя поля, а не значение, поэтому синтаксис остаётся управляемым и не открывает дыр для инъекций. Временную связь в runtime объявляют так же, как постоянную, но прямо в вызове:

<?php
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;

$rows = ProductTable::getList([
    'select'  => ['ID', 'NAME', 'STORE_AMOUNT' => 'STORE.AMOUNT'],
    'runtime' => [
        new Reference(
            'STORE',
            StoreProductTable::class,
            Join::on('this.ID', 'ref.PRODUCT_ID')
        ),
    ],
])->fetchAll();

Runtime-поля превращают ORM в мощный инструмент отчётности: сложные аналитические выборки, которые раньше требовали сырого SQL, теперь собираются типизированно и безопасно. Это особенно ценно в highload-проектах, где отчётов много и цена ошибки в запросе высока.

Кэширование выборок в ORM

Выборки данных — частый источник нагрузки, и D7 даёт кэширование прямо на уровне getList. Достаточно передать параметр cache с временем жизни в секундах — и результат запроса сохранится, а при повторном вызове с теми же параметрами отдастся из кэша, минуя базу.

<?php
$rows = OrdersLogTable::getList([
    'select' => ['ID', 'ORDER_ID', 'STATUS'],
    'filter' => ['=STATUS' => 'paid'],
    'cache'  => [
        'ttl'         => 3600,   // время жизни кэша, секунд
        'cache_joins' => true,   // кэшировать и результаты join-ов
    ],
])->fetchAll();

Ключевой вопрос любого кэша — инвалидация (когда сбрасывать). В ORM кэш можно связать с тегированной инвалидацией: при изменении данных сущности через add/update/delete ORM способна сбросить связанные теги, и устаревшая выборка перестаёт отдаваться. Это тот же принцип, что и в теговом кэше компонентов: не «протухание по времени», а точечный сброс по факту изменения. Для справочных данных, которые редко меняются (валюты, характеристики, регионы), это даёт почти бесплатную скорость.

Практический ориентир такой. Кэшируйте выборки, которые (а) тяжёлые или частые и (б) меняются редко. Не кэшируйте персональные и постоянно меняющиеся данные с длинным TTL — получите «залипшие» значения. И помните: кэш ORM — это уровень запросов; над ним есть кэш компонентов и композит. Как выстроить всю пирамиду кэширования, чтобы слои не мешали друг другу, подробно разобрано в материале про кэширование в 1С-Битрикс.

Транзакции в D7

ORM отлично читает и пишет одиночные записи, но по умолчанию не оборачивает цепочку операций в транзакцию — каждый add/update/delete это отдельный запрос, который применяется сразу. Когда несколько изменений должны сработать «всё или ничего» (списать остаток и создать документ отгрузки; провести оплату и обновить статус заказа), транзакцию открывают явно через соединение с базой.

<?php
use Bitrix\Main\Application;

$connection = Application::getConnection();
$connection->startTransaction();

try {
    // Несколько операций, которые должны примениться вместе
    StoreProductTable::update($storeRowId, ['AMOUNT' => $newAmount]);
    ShipmentTable::add([
        'ORDER_ID' => $orderId,
        'STATUS'   => 'ready',
    ]);

    $connection->commitTransaction();   // всё прошло — фиксируем
} catch (\Throwable $e) {
    $connection->rollbackTransaction(); // ошибка — откатываем всё
    throw $e;                           // не глотаем исключение молча
}

Схема стандартна для любой работы с транзакциями: открыли (startTransaction), выполнили операции внутри try, при успехе зафиксировали (commitTransaction), при исключении откатили (rollbackTransaction). Ключевое — не глотать исключение: после отката его нужно пробросить или залогировать, иначе вы получите молчаливую потерю данных.

Где транзакции критичны: в собственных таблицах и highload-блоках, где нет встроенной бизнес-логики модуля, которая подстраховала бы целостность. Для сущностей ядра (заказ в Bitrix\Sale) многое уже обёрнуто внутри объектного API — там свои механизмы сохранения. Но как только вы пишете напрямую в несколько своих таблиц, ответственность за целостность на вас, и транзакция обязательна.

HighloadBlock через ORM

HighloadBlock (highload-инфоблок) — механизм Битрикса для хранения больших объёмов однотипных данных в отдельной таблице, а не в универсальной структуре обычных инфоблоков. Он с самого начала спроектирован под D7: у каждого highload-блока есть своя сущность ORM, и работа с ним — это чистая ORM, без старого API вообще.

Чтобы получить DataManager highload-блока, его сущность нужно «скомпилировать» по идентификатору блока. Дальше всё как с обычным табличным классом:

<?php
use Bitrix\Main\Loader;
use Bitrix\Highloadblock\HighloadBlockTable as HLBT;

Loader::includeModule('highloadblock');

// 1. Получаем описание highload-блока по ID
$hlblock = HLBT::getById($hlblockId)->fetch();

// 2. Компилируем сущность и берём её класс DataManager
$entity = HLBT::compileEntity($hlblock);
$dataClass = $entity->getDataClass();

// 3. Работаем как с обычным DataManager
$rows = $dataClass::getList([
    'select' => ['ID', 'UF_XML_ID', 'UF_NAME'],
    'filter' => ['=UF_ACTIVE' => 1],
    'order'  => ['UF_NAME' => 'ASC'],
    'limit'  => 100,
])->fetchAll();

// Добавление записи
$dataClass::add([
    'UF_XML_ID' => 'sku-001',
    'UF_NAME'   => 'Красный',
    'UF_ACTIVE' => 1,
]);

Обратите внимание: поля highload-блока — это пользовательские свойства с префиксом UF_, и обращаетесь вы к ним так же, как к любым полям ORM. Всё, что мы обсуждали, — связи, рантаймы, агрегаты, кэш, транзакции — работает и здесь, потому что это обычная сущность DataManager.

Именно поэтому highload-блоки идеальны для справочников на сотни тысяч записей, характеристик товаров, внешних каталогов и логов: отдельная таблица снимает нагрузку с универсальных таблиц инфоблоков, а типизированная ORM делает работу удобной. Когда использовать highload-блоки вместо обычных инфоблоков и как их проектировать под нагрузку — тема отдельной статьи про highload-блоки в 1С-Битрикс.

Чем D7 лучше CIBlockElement::GetList и CModule

Соберём преимущества D7 предметно, на конкретных болях старого API. Речь не про «новое ради нового», а про измеримые выигрыши в разработке и эксплуатации.

Честно о минусах: у D7 порог входа выше — нужно понимать ООП, namespaces и саму модель ORM. И не вся функциональность ядра имеет полноценный D7-эквивалент, поэтому местами приходится возвращаться к старому API. Но для новой логики выигрыш перевешивает почти всегда.

Таблица: старое API против D7

Чтобы держать соответствие под рукой, вот компактная «шпаргалка перевода» типичных задач со старого API на D7. Она же — карта того, что искать в D7, когда переписываете старый код.

ЗадачаСтарое APID7 / ORM
Подключить модульCModule::IncludeModule('iblock')Loader::includeModule('iblock')
Выбрать элементыCIBlockElement::GetList(...)ElementTable::getList([...])
Выбрать разделыCIBlockSection::GetList(...)SectionTable::getList([...])
Выбрать пользователейCUser::GetList(...)UserTable::getList([...])
Дата и времяMakeTimeStamp, строкиBitrix\Main\Type\DateTime
Соединение с БДглобальный $DBApplication::getConnection()
Параметры запроса$_GET, $_POST...getContext()->getRequest()
Работа с файламиCFileBitrix\Main\FileTable, IO-классы
Результат операцииfalse / ID / глобальобъект Result

Важная оговорка: не для каждой строки соответствие «один в один». Например, сохранение элемента инфоблока со всей бизнес-логикой (события, права, пересчёт) в старом API делает CIBlockElement::Add/Update, и полноценной «сырой» ORM-замены с той же логикой у него нет — там нужен либо тот же класс, либо объектный API инфоблоков. Поэтому таблица — ориентир, а не жёсткое правило. Чтение почти всегда переводится на ORM свободно; запись сущностей ядра — осторожно.

Совместимость старого и нового кода

Главный страх при переходе на D7 — «придётся всё переписать». Не придётся. Старое и новое ядро сосуществуют в одной установке, работают с одной базой и вызываются в одном скрипте. Это позволяет мигрировать эволюционно, а не революцией.

На практике это выглядит буднично. Вы можете в одном методе выбрать данные через ORM, а сохранить через старый класс, если он даёт нужную бизнес-логику:

<?php
use Bitrix\Main\Loader;
use Bitrix\Iblock\ElementTable;

Loader::includeModule('iblock');

// Чтение — современно, через ORM
$rows = ElementTable::getList([
    'select' => ['ID', 'NAME', 'CODE'],
    'filter' => ['=IBLOCK_ID' => 12, '=ACTIVE' => 'Y'],
    'limit'  => 20,
])->fetchAll();

// Запись элемента инфоблока — через старый класс,
// чтобы отработали события, права и пересчёт свойств
$el = new \CIBlockElement();
$el->Update($rows[0]['ID'], ['NAME' => $rows[0]['NAME'] . ' (обновлён)']);

Это не «плохой код» — это прагматичное использование сильных сторон каждого слоя. Правило гигиены такое: в пределах одной логической операции старайтесь не смешивать слои без нужды (чтобы не запутать поведение событий и кэша), но в пределах проекта совмещать нормально и правильно. Читайте на D7, а пишите тем API, который даёт корректную бизнес-логику для конкретной сущности.

Отдельно про события. У D7 своя событийная модель (Bitrix\Main\EventManager, обработчики через ORM-события onBeforeAdd, onAfterUpdate и т. д.), но многие старые события продолжают работать. При смешивании важно понимать, какие обработчики сработают при вашем способе записи, — иначе можно получить «пропавший» обмен с 1С или несработавшую бизнес-логику.

Миграция со старого API: пошагово

Перевод проекта на D7 — не разовое событие, а планомерная работа. Ниже — порядок, который на практике даёт результат без остановки разработки и без риска «всё сломать одним коммитом».

  1. Начните с нового кода. Договоритесь в команде: вся новая логика пишется на D7. Это останавливает рост legacy, даже если старое пока не трогаете.
  2. Переводите чтение раньше записи. Выборки (GetList) переносятся на ORM проще и безопаснее всего — здесь нет риска сломать бизнес-логику сохранения. Начинайте с них.
  3. Опишите свои сущности. Для собственных таблиц заведите классы-наследники DataManager с getMap(). Это разом даёт типизацию, валидацию и связи.
  4. Выносите повторяющиеся выборки в репозитории. Оберните типовые getList в методы сервисных классов — код перестанет дублироваться, а места для оптимизации станут очевидны.
  5. Запись сущностей ядра переводите осторожно. Там, где есть объектный API (заказы, корзина в Bitrix\Sale), переходите на него; где его нет — оставляйте старые классы, но изолируйте их за своим фасадом.
  6. Настройте автозагрузку своих классов. Оформите свой код как модуль или зарегистрируйте классы, чтобы require исчезли и появилась предсказуемая структура.
  7. Проверяйте события и кэш после каждого шага. Убедитесь, что обмен с 1С, права и теговый кэш продолжают работать. Миграция ORM не должна ломать инвалидацию.

Ключевая установка — никакого «большого переписывания». Проект живёт и приносит деньги; ваша задача — постепенно снижать долю legacy, начиная с самого безопасного (чтение) и оставляя рискованное (запись сущностей ядра) на потом. Через несколько итераций доля D7-кода становится преобладающей, а старое API остаётся только там, где оно действительно нужно.

Где старое API ещё оправдано

D7 — не догма. Есть места, где старое API остаётся правильным выбором, и притворяться, что это не так, — вредно. Понимание границы экономит время и уберегает от переусложнения.

Правильная стратегия — не «D7 везде любой ценой», а «D7 по умолчанию, старое API там, где оно объективно уместно». Оборачивайте старые вызовы в собственные сервисные классы: тогда снаружи ваш код выглядит современно, а внутри использует то API, которое даёт корректное поведение. Так вы получаете и чистую архитектуру, и надёжность.

Частые ошибки

Переход на D7 порождает свой набор типовых граблей. Зная их заранее, вы избежите большинства болезненных отладок.

Чек-лист перехода на D7

Короткий список, по которому удобно свериться при разработке и код-ревью нового кода на Битриксе:

Вывод

Ядро D7 — это не «другой Битрикс», а современный слой той же платформы: пространства имён и автозагрузка вместо ручных подключений, Application и сервис-локатор вместо глобальных объектов, ORM с DataManager вместо «магических массивов» старого API. Чтение данных через getList становится типизированным, читаемым и безопасным; связи через Reference убирают N+1; рантайм-поля и агрегаты собирают сложные отчёты одним запросом; кэш и транзакции решают вопросы скорости и целостности. HighloadBlock изначально живёт на ORM и закрывает задачи больших справочников.

Практический стержень — эволюция, а не революция. Старое и новое ядро сосуществуют, поэтому мигрировать можно постепенно: новый код на D7, чтение переводим раньше записи, свои таблицы описываем сущностями, а старое API оставляем там, где оно даёт корректную бизнес-логику. Так проект становится чище, безопаснее и быстрее в разработке, не теряя стабильности. Если нужен такой код под ваш проект — мы пишем его на D7 в рамках highload-проектов и разработки REST API для 1С-Битрикс. Смежные материалы по архитектуре — REST API в 1С-Битрикс, highload-блоки и вся рубрика «Разработка и архитектура».

Частые вопросы

Что такое D7 в 1С-Битрикс простыми словами?

D7 — это современное ядро платформы 1С-Битрикс, построенное на объектно-ориентированном PHP: классах, пространствах имён и автозагрузке. Оно пришло на смену старому «процедурному» ядру (так называемому ядру старого образца), где всё держалось на глобальных функциях и классах вида CIBlockElement, CModule, CUser. В D7 та же функциональность выражена через аккуратные пространства имён Bitrix\Main, Bitrix\Iblock, Bitrix\Sale и другие, а работа с данными идёт через ORM. Важно понимать: D7 не заменяет старое ядро полностью, а работает рядом с ним в той же системе — оба слоя доступны одновременно, и вы можете писать новый код на D7, не ломая старый.

Чем ORM и DataManager лучше CIBlockElement::GetList?

ORM в D7 даёт типизированную и читаемую работу с данными вместо «магических» строковых массивов старого API. Класс-наследник DataManager описывает таблицу и её поля явно, а метод getList принимает понятную структуру: select, filter, order, limit, runtime. Вы получаете подсказки IDE, безопасные параметризованные запросы (защиту от SQL-инъекций из коробки), связи между сущностями через Reference и join одним выражением, агрегаты и рантайм-поля. CIBlockElement::GetList решает те же задачи, но через нетипизированные массивы фильтров с префиксами вроде >=PROPERTY_*, где ошибку видно только в рантайме. Для новой логики ORM почти всегда предпочтительнее.

Как устроена автозагрузка классов в D7 и зачем нужны пространства имён?

В D7 классы не подключаются вручную через require: они загружаются автоматически по имени. Пространство имён класса отражает его расположение — например, Bitrix\Main\Type\DateTime лежит в модуле main по стандартизированному пути. Когда PHP встречает обращение к классу, автозагрузчик Битрикса по namespace находит нужный файл и подключает его сам. Это избавляет от десятков require, устраняет конфликты имён (в разных модулях могут быть классы с одинаковым коротким именем) и делает код предсказуемым: по полному имени класса сразу понятно, из какого модуля он и где искать исходник. Свои классы регистрируют в автозагрузке через loadModule или через описание в модуле.

Можно ли использовать старое API и D7 в одном проекте одновременно?

Да, и это нормальная практика. Старое и новое ядро сосуществуют в одной установке Битрикса: вы можете в одном скрипте вызвать CIBlockElement::GetList и тут же выбрать данные через ORM DataManager. Более того, во многих местах это неизбежно — часть функциональности (например, сохранение элемента инфоблока со всей бизнес-логикой, правами и событиями) до сих пор надёжнее делать штатными классами старого образца. Правило простое: новую логику пишите на D7, а старое API применяйте там, где оно даёт готовое поведение, которого нет в ORM, или где переписывание слишком рискованно. Смешивать в пределах проекта можно, смешивать бездумно в пределах одной операции — нежелательно.

Что такое runtime-поле в getList и когда оно нужно?

Runtime-поле — это виртуальное поле, которое вы добавляете к сущности прямо в момент выборки, не описывая его в самом классе DataManager. Через параметр runtime метода getList можно объявить временную связь (ReferenceField) с другой таблицей, вычисляемое выражение (ExpressionField) или агрегат (SUM, COUNT, AVG). Это нужно, когда связь или расчёт разовые и не имеют смысла как постоянная часть сущности: например, посчитать сумму заказов пользователя, приклеить к товарам их остаток на складе или сгруппировать строки и получить количество. Runtime-поля делают ORM гибкой — сложные отчётные выборки собираются одним getList без ручного SQL.

Как работает кэширование выборок в ORM D7?

У метода getList есть параметр cache, в котором задаётся время жизни кэша в секундах и, при необходимости, признак кэширования join-ов. Если кэш включён, результат выборки сохраняется и при повторном запросе с теми же параметрами отдаётся из кэша без обращения к базе. Кэш ORM можно связать с тегированной инвалидацией: при изменении данных через add, update и delete соответствующие теги сбрасываются, и устаревшая выборка перестаёт отдаваться. Это тот же принцип, что и теговый кэш компонентов, но на уровне запросов к данным. Для тяжёлых справочных выборок, которые редко меняются, кэш ORM даёт заметный выигрыш по скорости.

Нужны ли транзакции при работе через DataManager?

DataManager сам не оборачивает цепочку операций в транзакцию — каждый add, update или delete это отдельный запрос. Когда несколько изменений должны примениться «всё или ничего» (например, списать остаток и создать документ отгрузки), транзакцию открывают явно через соединение с базой: startTransaction, затем commitTransaction при успехе или rollbackTransaction при ошибке. Соединение берут из пула через Application и класс Bitrix\Main\Application::getConnection. Транзакции особенно важны в HighloadBlock и в собственных таблицах, где нет встроенной бизнес-логики модуля, которая подстраховала бы целостность данных.

Что такое HighloadBlock и почему его удобно использовать через ORM?

HighloadBlock (highload-инфоблок) — это механизм Битрикса для хранения больших объёмов однотипных данных в отдельной таблице, а не в универсальной структуре инфоблоков. Он изначально спроектирован под D7: у каждого highload-блока есть своя сущность ORM, класс которой можно получить по идентификатору блока через compileEntity. Дальше вы работаете с ним как с обычным DataManager — getList, add, update, delete, связи и рантаймы. Именно поэтому highload-блоки хорошо подходят для справочников на сотни тысяч записей, характеристик товаров и внешних каталогов: отдельная таблица плюс типизированная ORM дают и скорость, и удобство разработки. Подробнее мы разбираем это в отдельной статье про highload-блоки.

Поделиться:

Нужен современный код на 1С-Битрикс?

Пишем новую логику на ядре D7: чистая ORM, типизированные сущности и выборки, кэш и транзакции, highload-блоки под нагрузку. Переведём проект со старого API на современный стек без остановки разработки.

Игорь Воскресенский

Команда студии B2Bsite. С 2014 года разрабатываем, ускоряем и поддерживаем проекты на 1С-Битрикс: современное ядро D7 и ORM, чистые сущности и выборки, highload-блоки, REST API, кэш и обмен с 1С для среднего и крупного бизнеса.

← Все статьи блога