Как связать таблицы в D7 ORM 1С-Битрикс через Reference и runtime?

Как связать таблицы в D7 ORM 1С-Битрикс через Reference и runtime?

На практике вы уже умеете getList по одной таблице, но при добавлении в select "USER.NAME" ORM отвечает Unknown field, JOIN пустой, а в профайлере сотни запросов в цикле — типичная проблема, когда связь между таблицами не описана. Например, забыли runtime или скопировали INNER JOIN с форума, и половина заказов исчезает из выгрузки. Связь таблиц в D7 ORM - это Reference в getMap или runtime ReferenceField с Join::on('this.FIELD', 'ref.ID'). Ниже - как за один getList подтянуть автора, раздел или Highload, собрать цепочку из трех таблиц и проверить SQL без сырого JOIN вручную.

Reference - это описание связи между двумя ORM-таблицами: "поле this.CREATED_BY равно ref.ID". Постоянную связь кладут в getMap класса *Table, разовую - в runtime getList. По умолчанию LEFT JOIN: строки без связи не пропадают. Для select с точкой нужен псевдоним runtime. Цепочка из трех таблиц строится через this.alias.FIELD. Перед продом проверьте getQuery() и счетчик запросов.

Материал про коробочную CMS "1С-Битрикс: Управление сайтом", не про облачный Bitrix24. ORM (Object-Relational Mapping) - слой D7, который переводит PHP-вызовы getList в SQL без ручного написания JOIN. Reference - не магия: это декларация "какое поле нашей таблицы ссылается на какое поле чужой". Если своя *Table еще не создана, начните с гайда по DataManager и getMap; каркас модуля - в инструкции по созданию модуля. Дальше предполагаем, что OrderTable или аналог уже лежит в lib/ и getList по одной таблице работает.

Выберите, когда JOIN в ORM, а когда цикл или CIBlockElement

Сравнительная таблица: когда JOIN в ORM, а когда цикл или CIBlockElement

Reference не заменяет все выборки. Это инструмент, когда одна строка вашей таблицы ссылается на другую сущность по внешнему ключу (FK).

Сценарий Цикл getList CIBlockElement (B60) ORM + Reference
10 заказов + имя автора 11 SQL (N+1) Тяжелый GetList 1 JOIN - оптимально
Каталог 5000 SKU + 5 свойств Тысячи запросов Привычный API Риск декартова произведения
Лог модуля + UserTable Медленно на проде Избыточно Да - связь в getMap
HL-справочник + свой HL N+1 Нет Runtime Reference к compileEntity

Сравнение старого GetList и ElementTable - в статье про CIBlockElement vs D7 ORM. CRUD Highload без углубления в JOIN - в гайде по HL и compileEntity.

Делайте: один getList с JOIN, когда связей мало и нужны поля соседней таблицы. Не делайте: не тяните пять множественных свойств инфоблока в один запрос - получите 15x7x11 строк вместо 33.

Настройте постоянный Reference в getMap модуля

Схема настройки постоянного Reference в getMap модуля Битрикс

Если связь OrderTable - UserTable повторяется в каждом отчете, опишите ее один раз в getMap. Ядро само добавит LEFT JOIN при select с точкой.

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

// в getMap() класса OrderTable:
new Reference(
    'USER',
    UserTable::class,
    Join::on('this.CREATED_BY', 'ref.ID')
),

this - текущая таблица (OrderTable), ref - связанная (UserTable). INNER JOIN, когда нужны только строки с существующим автором:

Join::on('this.CREATED_BY', 'ref.ID')
    ->where('ref.ACTIVE', 'Y')
    ->configureJoinType('inner')

В проектах встречаются два namespace: новый Bitrix\Main\ORM\Fields\Relations\Reference и legacy ReferenceField из Bitrix\Main\Entity. Синтаксис Join::on одинаковый, отличия в имени класса и способе передачи join_type. После добавления Reference в getMap select USER.NAME работает без runtime в каждом вызове.

Делайте: храните FK в миграции Sprint до Reference. Не делайте: не ставьте INNER по умолчанию - пропадут заказы без автора.

Добавьте runtime ReferenceField для разового JOIN

Чеклист: runtime ReferenceField для разового JOIN в D7 ORM

Runtime живет только в рамках одного getList. Подходит для админ-отчета или связи с ElementTable, которую не хотите тащить в getMap навсегда.

  1. Задайте псевдоним в runtime, например USER.
  2. Укажите Join::on('this.CREATED_BY', 'ref.ID') - поле this из основной таблицы, ref из UserTable.
  3. Добавьте в select поля с точкой: 'AUTHOR_NAME' => 'USER.NAME', 'AUTHOR_LOGIN' => 'USER.LOGIN'.
  4. Проверьте filter: фильтр по USER.LOGIN требует того же псевдонима в runtime.
  5. Выведите SQL через $query = OrderTable::query(); ... echo $query->getQuery();
  6. Сравните с профайлером: один запрос вместо цикла по ID.
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\UserTable;

OrderTable::getList([
    'select' => [
        'ID', 'SUM',
        'AUTHOR_NAME' => 'USER.NAME',
    ],
    'runtime' => [
        new ReferenceField(
            'USER',
            UserTable::class,
            ['=this.CREATED_BY' => 'ref.ID'],
            ['join_type' => 'LEFT']
        ),
    ],
]);

Ошибка Unknown field USER.NAME почти всегда значит: забыли runtime или опечатка в псевдониме. Пустой AUTHOR_NAME при корректном SQL - проверьте CREATED_BY: ноль или битый FK. ExpressionField для COUNT или CONCAT тоже кладут в runtime рядом с Reference - ядро соберет агрегат в том же запросе.

Workflow одного отчета:
OrderTable::getList → runtime USER (ReferenceField) → select USER.NAME → getQuery() сверяем LEFT JOIN → профайлер показывает 1 SQL → выводим в админке без цикла по CREATED_BY

Делайте: копируйте рабочий runtime в сервис модуля. Не делайте: не ожидайте, что runtime из прошлого getList подхватится в следующем.

Соберите цепочку из трех и более таблиц

Второй JOIN ссылается не на корень, а на предыдущий runtime-алиас. Классический кейс: раздел - элемент - свойство.

'runtime' => [
    new ReferenceField('SE', ElementTable::class,
        ['=this.IBLOCK_SECTION_ID' => 'ref.ID']),
    new ReferenceField('PROP', PropertyTable::class,
        ['=ref.IBLOCK_ELEMENT_ID' => 'this.SE.ID']),
],
'select' => ['ID', 'PROP_VALUE' => 'PROP.VALUE'],

Ключевая строка: this.SE.IBLOCK_ELEMENT_ID - this указывает на алиас SE, а не на корневую таблицу. На форуме dev.1c-bitrix.ru это главный ответ на "можно ли в JOIN только this и ref".

ManyToMany через промежуточную таблицу описывают отдельной сущностью. Если в связующей таблице есть QUANTITY или дата - нужен OneToMany к посреднику, а не голый ManyToMany: к полям посредника ManyToMany не дает доступ.

Делайте: для HL используйте compileEntity и Reference на второй HL-блок. Не делайте: не копируйте алиасы вроде this.se с форума вслепую - назовите понятно: SECTION, AUTHOR, SKU.

Проверьте производительность: cache_joins и decompose

Выборки с JOIN по умолчанию не кешируются. Для редко меняющихся справочников:

'cache' => ['ttl' => 3600, 'cache_joins' => true]

При связи 1:N в одном запросе с limit ядро может размножить строки. Официальная документация рекомендует QueryHelper::decompose() - разбор результата на честные сущности без декартова произведения. На публичном каталоге с десятками свойств чаще выгоднее отдельные легкие запросы, чем один тяжелый JOIN.

Схема отладки: OrderTable::getList → профайлер (1 SQL?) → getQuery() (LEFT или INNER?) → индекс на CREATED_BY в MySQL. Типичная история с прода: разработчик скопировал INNER JOIN из примера инфоблока, и половина заказов с CREATED_BY = 0 исчезла из выгрузки. Смена на LEFT вернула строки; пустое имя автора обработали в шаблоне как "Система".

Делайте: явный select только нужных полей. Не делайте: не ставьте JOIN на главную витрину без замеров на staging.

Пройдите чек-лист перед выкладкой на прод

  1. Убедитесь, что FK есть в БД и проиндексирован.
  2. Выберите getMap для постоянных связей, runtime - для разовых.
  3. Сверьте итоговый SQL: ожидаемый LEFT/INNER и один round-trip.
  4. Откройте профайлер: нет N+1 при списке из 50+ строк.
  5. Вынесите сборку запроса в метод сервиса модуля в lib/.
  6. Сбросьте кеш компонента после записи в связанную таблицу.

Нужна помощь со связями в кастомном модуле на реальном проекте - обсудим задачу. Примеры внедрений - в портфолио.

Автор: Максим Мольков, разработчик 1С-Битрикс.
Источники: документация по отношениям ORM, выборка данных и runtime, практика внедрений на CMS.

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

Чем Reference в getMap отличается от ReferenceField в runtime?

getMap описывает связь навсегда: любой getList по OrderTable может взять USER.NAME без повторной настройки. Runtime добавляет JOIN только в текущий вызов и исчезает в следующем. Для отчетов раз в квартал - runtime; для поля автора в каждом списке - getMap.

Как связать две своих таблицы в модуле?

Добавьте IntegerField с FK в getMap дочерней таблицы, создайте индекс в миграции, затем Reference на родительскую *Table через Join::on('this.PARENT_ID', 'ref.ID'). Проверьте select с точкой и getQuery().

Почему select USER.NAME возвращает пустое значение?

Сначала убедитесь, что runtime с псевдонимом USER зарегистрирован в том же getList. Затем проверьте CREATED_BY: NULL или 0 даст пустое имя при корректном LEFT JOIN. При INNER строка может исчезнуть целиком.

Что означают this и ref в Join::on?

this - поля основной таблицы запроса (OrderTable). ref - поля связанной сущности (UserTable). Запись Join::on('this.CREATED_BY', 'ref.ID') читается как "CREATED_BY заказа равен ID пользователя".

Можно ли связать ORM с Highload-блоком?

Да. Получите DataManager через compileEntity HL-блока и передайте его класс в Reference или ReferenceField так же, как UserTable. Подробности CRUD HL - в отдельном гайде по Highload; здесь достаточно runtime JOIN по полю-ссылке.

Когда LEFT JOIN менять на INNER?

INNER оставляет только строки, где связь найдена. Используйте для отчетов "только с автором" через configureJoinType('inner'). Для списков заказов, где часть записей без пользователя, оставляйте LEFT по умолчанию.

Читайте также

Интеграции с 1С и API
1074 6 мин.

Как настроить регистрацию и личный кабинет покупателя на 1С-Битрикс?

Пошаговая настройка регистрации на сайте битрикс: Главный модуль, main.register, system.auth.form и sale.personal.section с историей заказов и 152-ФЗ.
Интеграции с 1С и API
941 6 мин.

Как настроить скидки и промокоды на 1С-Битрикс: правила корзины и купоны

Пошаговый гайд по скидкам в CMS-магазине: скидка на товар, правило корзины от суммы, купоны с лимитом, приоритеты без конфликтов и тестовый заказ. Не Bitrix24.
Интеграции с 1С и API
759 15 мин.

Что такое компонент в Битриксе и как он работает

Каждый разработчик, впервые столкнувшийся с Битриксом, проходит через своеобразный обряд посвящения. Вначале кажется, что это просто CMS, где можно поправить HTML в визуальном редакторе или дописать пару строк CSS. Но однажды наступает момент, когда нужно изменить логику вывода новостей, отфильтровать товары по хитрому свойству или добавить на страницу нечто совершенно новое. И тут он впервые слышит это слово — «компонент». Для многих этот момент становится стеной. Система, казавшаяся понятной, вдруг превращается в черный ящик, полный непонятных файлов и странных переменных. Но стоит лишь раз заглянуть под капот, как эта стена рассыпается, превращаясь в набор удивительно логичных и мощных строительных блоков. Понимание компонентов — это тот самый щелчок, после которого разработка на Битрикс из мучения превращается в творчество.

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

Интеграции с 1С и API
718 2 мин.

Установка Composer в 1С-Битрикс

Установка Composer в проекте на 1С-Битрикс требует учета особенностей платформы, чтобы обеспечить корректную работу и интеграцию с системой.