29.08.2019

runtime Отношения в Битрикс ORM D7

ORM D7 в Битрикс появился уже довольно давно. Очень удобно использовать одинаковые методы для запросов к БД с помощью ::getList, просто описывать свои таблицы с помощью ::getMap.
Например вот так можно выбрать информацию о товарах вместе с данными о доступном количестве и зарезервированном количестве + сразу же вывести и название товара.

$products = \Bitrix\Catalog\ProductTable::getList([
    "select" => [
        "PROD_ID" => "ID",
        "NAME" => "IBLOCK_ELEMENT.NAME",
        "IBLOCK_ID" => "IBLOCK_ELEMENT.IBLOCK_ID",
        "TYPE"
    ],
    "filter" => [
        "!TYPE" => \Bitrix\Catalog\ProductTable::TYPE_SKU,
        "IBLOCK_ELEMENT.ACTIVE" => "Y"
    ],
    "order" => [
        "QUANTITY" => "ASC",
        "QUANTITY_RESERVED" => "DESC",
    ]
])->fetchAll();

При использовании старого API: запрос к CCatalogProduct + запрос к CIblockElement мы бы получили вместо одного запроса - два. Да и нужно было бы писать гораздо больше кода для этого. Возможность работы с данными из других таблиц появилась благодаря указанию отношений в методе getMap. Но что делать если по какой то причине эта связь не указана?
В таком случае стоит воспользоваться секцией 'runtime' в массиве параметров метода getList.
Например так:

$dbOrders = \Bitrix\Sale\Internals\OrderTable::getList([
    "select" => [
        "*",
        "PROPERTY_VALUE.*",
    ],
    "filter" => [
        "PROPERTY_VALUE.PROPERTY.CODE" => "LOCATION",
        "!STATUD_ID" => ["C", "RS"]
    ],
    'runtime' => [
        'PROPERTY_VALUE' => [
            'data_type' => \Bitrix\Sale\Internals\OrderPropsValueTable::class,
            'reference' => [
                '=this.ID' => 'ref.ORDER_ID',
            ]
        ],
    ],
    "order" => [
        "ID" => "ASC"
    ]
]);

Данный запрос выведет список заказов в статусах C и RS сразу со значением свойства LOCATION. И все это одним запросом.

Вместо ассоциативного массива внутри runtime для каждого поля можно использовать обьекты класса \Bitrix\Main\Entity\ReferenceField. Тогда PROPERTY_VALUE из предыдущего примера будет выглядеть так:

new \Bitrix\Main\Entity\ReferenceField(
    'PROPERTY_VALUE',
    '\Bitrix\Sale\Internals\OrderPropsValue',
    array(
        '=this.ID' => 'ref.ORDER_ID',
    )
)

При добавлении \Bitrix\Main\Entity\ReferenceField в getList, во время формирования SQL запроса в секцию FROM будет добавлен JOIN. Тип JOIN можно указать так:

'PROPERTY_VALUE' => [
    'data_type' => \Bitrix\Sale\Internals\OrderPropsValueTable::class,
    'reference' => [
        '=this.ID' => 'ref.ORDER_ID',
    ],
    ['join_type' => 'LEFT']
]

Рассмотрим другой пример:

$cities = \Bitrix\Sale\Location\LocationTable::getList([
    "select" => ["ID"],
    "filter" => [
        "TYPE_ID" => 5,
        "ZIP.XML_ID" => false
    ],
    'runtime' => [
        'ZIP' => [
            'data_type' => \Bitrix\Sale\Location\ExternalTable::class,
            'reference' => [
                '=this.ID' => 'ref.LOCATION_ID',
                '=ref.SERVICE_ID' => new \Bitrix\Main\DB\SqlExpression('?i', $zipExternalId),
            ],
            ['join_type' => 'LEFT']
        ],
    ]
])->fetchAll();

В этом примере будут выбраны все местоположения для которых не заполнен почтовый индекс. В этом примере стоит обратить внимание на то, что в условиях ReferenceField есть такая конструкция '=ref.SERVICE_ID' => new \Bitrix\Main\DB\SqlExpression('?i', $zipExternalId),. С ее помощью в запрос будет подставлен ID внешнего сервиса отвечающего за почтовые индексы. Если этого не сделать то объединение таблиц произойдет по всем доступным сервисам. При этом условие будет добавлено не в секцию WHERE SQL запроса, а в соответствующий JOIN. Для этого примера SQL запрос будет выглядеть так:

SELECT 
    `sale_location_location`.`ID` AS `ID`
FROM `b_sale_location` `sale_location_location` 
    LEFT JOIN `b_sale_loc_ext` `sale_location_location_zip` ON `sale_location_location`.`ID` = `sale_location_location_zip`.`LOCATION_ID` AND `sale_location_location_zip`.`SERVICE_ID` = 2
WHERE `sale_location_location`.`TYPE_ID` = 5
AND (`sale_location_location_zip`.`XML_ID` IS NULL OR `sale_location_location_zip`.`XML_ID` = '')