На практике вы уже умеете 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
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 модуля
Если связь 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 живет только в рамках одного getList. Подходит для админ-отчета или связи с ElementTable, которую не хотите тащить в getMap навсегда.
- Задайте псевдоним в runtime, например USER.
- Укажите Join::on('this.CREATED_BY', 'ref.ID') - поле this из основной таблицы, ref из UserTable.
- Добавьте в select поля с точкой: 'AUTHOR_NAME' => 'USER.NAME', 'AUTHOR_LOGIN' => 'USER.LOGIN'.
- Проверьте filter: фильтр по USER.LOGIN требует того же псевдонима в runtime.
- Выведите SQL через $query = OrderTable::query(); ... echo $query->getQuery();
- Сравните с профайлером: один запрос вместо цикла по 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.
Пройдите чек-лист перед выкладкой на прод
- Убедитесь, что FK есть в БД и проиндексирован.
- Выберите getMap для постоянных связей, runtime - для разовых.
- Сверьте итоговый SQL: ожидаемый LEFT/INNER и один round-trip.
- Откройте профайлер: нет N+1 при списке из 50+ строк.
- Вынесите сборку запроса в метод сервиса модуля в lib/.
- Сбросьте кеш компонента после записи в связанную таблицу.
Нужна помощь со связями в кастомном модуле на реальном проекте - обсудим задачу. Примеры внедрений - в портфолио.
Автор: Максим Мольков, разработчик 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 по умолчанию.
Было полезно?
Поставьте лайк, если статья помогла разобраться.
Поделиться статьёй
Отправьте тому, кому это пригодится.
Можно поделиться и фрагментом: выделите текст статьи.