Главная » Разработка Битрикс » D7 » referenceField и связи между таблицами в ORM Битрикс

referenceField и связи между таблицами в ORM Битрикс

Схема связи таблиц в ORM Битрикс: Reference соединяет сущности и позволяет выбрать данные одним запросом

Данные в базе разложены по таблицам, и почти любая полезная выборка требует их соединить: заказы вместе с пользователями, элементы вместе с разделами, записи справочника вместе с привязанными к ним значениями.

В 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 — и индексы на полях, по которым таблицы соединяются. И главное: описанная связь имеет смысл только тогда, когда вы перестаёте делать запросы в цикле.

    Нужна помощь с Битрикс?

    Исправим ошибку, доработаем сайт, ускорим Битрикс или поможем разобраться с проблемой.

    Услуги
    Инструменты
    База знаний