Данные в базе разложены по таблицам, и почти любая полезная выборка требует их соединить: заказы вместе с пользователями, элементы вместе с разделами, записи справочника вместе с привязанными к ним значениями.
В ORM 1С-Битрикс за это отвечает referenceField — описание связи
между сущностями. Один раз описав связь, дальше вы обращаетесь к полям связанной
таблицы как к своим. Разберём, как это устроено, и почему связи стоит описывать
в сущности, а не собирать данные циклом.
Проблема, которую решает reference
Классический антипаттерн — запрос внутри цикла:
<?php
$orders = OrderTable::getList(['select' => ['ID', 'USER_ID']])->fetchAll();
foreach ($orders as $order) {
// отдельный запрос на каждый заказ
$user = UserTable::getById($order['USER_ID'])->fetch();
$order['USER_NAME'] = $user['NAME'];
}
Сто заказов — сто один запрос к базе. На тысяче записей страница начинает открываться секундами. Связь решает это одним запросом.
Описание связи в сущности
Связь объявляется в методе getMap() той сущности, откуда вы будете
делать выборку:
<?php
namespace Project\Entity;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
class OrderTable extends DataManager
{
public static function getTableName()
{
return 'project_order';
}
public static function getMap()
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('USER_ID'),
new StringField('NUMBER'),
new Reference(
'USER', // имя связи
\Bitrix\Main\UserTable::class, // с чем связываем
Join::on('this.USER_ID', 'ref.ID') // условие соединения
),
];
}
}
Ключевая конструкция — Join::on():
| Обозначение | Что означает |
|---|---|
this | Текущая сущность, в которой описывается связь |
ref | Сущность, на которую ссылаемся |
Нужна помощь с Битрикс?
Использование в выборке
После описания связи поля второй таблицы доступны через точку:
<?php
$result = OrderTable::getList([
'select' => [
'ID',
'NUMBER',
'USER_NAME' => 'USER.NAME',
'USER_EMAIL' => 'USER.EMAIL',
],
'filter' => [
'=USER.ACTIVE' => 'Y',
],
'order' => ['USER.LAST_NAME' => 'ASC'],
]);
while ($row = $result->fetch()) {
echo $row['NUMBER'] . ' — ' . $row['USER_NAME'];
}
Всё это — один запрос с соединением таблиц. Связанные поля работают
и в select, и в filter, и в order.
Получение всех полей связанной сущности
<?php
$result = OrderTable::getList([
'select' => ['ID', 'NUMBER', 'USER'], // все поля пользователя
]);
Удобно, но осторожно: при широкой связанной таблице это тянет много лишних данных. Перечислять нужные поля явно почти всегда лучше.
Тип соединения
По умолчанию используется LEFT JOIN: записи основной таблицы возвращаются
даже без пары в связанной. Тип можно изменить:
<?php
new Reference(
'USER',
\Bitrix\Main\UserTable::class,
Join::on('this.USER_ID', 'ref.ID'),
['join_type' => 'INNER']
),
| Тип | Поведение |
|---|---|
LEFT |
Все записи основной таблицы; поля связанной пусты при отсутствии пары |
INNER |
Только записи, у которых есть пара в связанной таблице |
RIGHT |
Применяется редко |
Практическое следствие: если после добавления связи из выборки внезапно пропала
часть записей — вероятно, стоит INNER там, где нужен LEFT.
Условия в самой связи
В Join::on() можно добавить дополнительные условия — они попадут
в ON, а не в WHERE:
<?php
new Reference(
'ACTIVE_USER',
\Bitrix\Main\UserTable::class,
Join::on('this.USER_ID', 'ref.ID')
->where('ref.ACTIVE', 'Y')
),
Разница существенна при LEFT JOIN. Условие в ON оставит
запись основной таблицы с пустыми полями связанной, а такое же условие
в filter уберёт саму запись из результата.
Связь без правки сущности
Если менять чужую сущность нельзя — например, это класс ядра, — связь можно
задать прямо в запросе через runtime:
<?php
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
$result = SomeTable::getList([
'select' => ['ID', 'USER_NAME' => 'USER.NAME'],
'runtime' => [
new Reference(
'USER',
\Bitrix\Main\UserTable::class,
Join::on('this.USER_ID', 'ref.ID')
),
],
]);
Такой способ удобен для разовых задач. Если связь нужна регулярно —
описывайте её в getMap(), чтобы не дублировать код.
Цепочки связей
Связи можно проходить насквозь, если промежуточные сущности их описывают:
<?php
$result = OrderTable::getList([
'select' => [
'ID',
'CITY' => 'USER.PROFILE.CITY',
],
]);
Каждый уровень — дополнительное соединение таблиц. Цепочка из четырёх-пяти уровней на большой выборке способна работать медленнее, чем два отдельных запроса, поэтому глубину стоит держать разумной.
Производительность
- Индексы обязательны. Поля, по которым идёт соединение, должны быть проиндексированы в обеих таблицах. Без этого база будет перебирать записи полностью.
- Перечисляйте поля явно. Выборка всей связанной сущности тянет лишние данные.
- Следите за глубиной. Каждый уровень цепочки — ещё одно соединение.
-
Проверяйте итоговый запрос. Метод
getQuery()->getQuery()возвращает готовый SQL — его полезно прогнать черезEXPLAIN.
Общее устройство ORM и остальные возможности выборки разобраны в статьях ORM в Битрикс и Query Builder в Битрикс.
Частые ошибки
-
Путаница
thisиref. Соединение строится не по тем полям, результат пустой или неверный. -
Условие в
filterвместоON. ПриLEFT JOINпропадают записи, которые должны были остаться. - Нет индексов на полях соединения. Запрос работает, но крайне медленно на объёме.
- Выборка всей связанной сущности. Лишний трафик и память.
- Связь описана, но выборка идёт циклом. Встречается чаще, чем кажется: код написан по старой привычке.
Итог
Reference описывает связь между сущностями ORM и позволяет получать
данные из нескольких таблиц одним запросом. Связь объявляется в getMap()
через Join::on('this.FIELD', 'ref.FIELD'), а дальше связанные поля
доступны через точку в select, filter и order.
Две вещи, которые определяют результат: правильный тип соединения —
LEFT или INNER — и индексы на полях, по которым
таблицы соединяются. И главное: описанная связь имеет смысл только тогда,
когда вы перестаёте делать запросы в цикле.
