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` = '')
Так же есть возможность настраивать связь с другими таблицами с помощью класса Bitrix\Main\ORM\Fields\Relations\Reference
.
Дальше в примерах будет приводиться PHP код для описания runtime поля и примерный SQL код, который наглядно показывает изменения в запросе
Связь с другой таблицей
(new Reference(
"USER", //Символьный код, который можно использовать в select и filter для ссылки на эту таблицу
\Bitrix\Main\UserTable::class, //orm клас
\Bitrix\Main\ORM\Query::on("this.USER_ID", "ref.ID"))//по какому полю выполняем join
);
//LEFT JOIN b_user on this.USER_ID = b_user.ID
Связь с другой таблицей и настройкой JOIN
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
\Bitrix\Main\ORM\Query::on("this.FILE_ID", "ref.ID")
))->configureJoinType(
\Bitrix\Main\ORM\Query\Join::TYPE_INNER,
// \Bitrix\Main\ORM\Query\Join::TYPE_LEFT,
// \Bitrix\Main\ORM\Query\Join::TYPE_LEFT_OUTER,
// \Bitrix\Main\ORM\Query\Join::TYPE_RIGHT,
// \Bitrix\Main\ORM\Query\Join::TYPE_RIGHT_OUTER,
);
//INNER JOIN b_file on this.FILE_ID = b_file.ID
Связь с другой таблицей и доп условия фильтрации в JOIN
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
Join::on("this.FILE_ID", "ref.ID")
->where("ref.HEIGHT", "<=", 1000)
->where("ref.WIDTH", "<=", 1000)
));
//LEFT JOIN b_file on this.FILE_ID = b_file.ID and b_file.HEIGHT <= 1000 and b_file.WIDTH <= 1000
То же самое, но только вместо where используется addCondition
и объект \Bitrix\Main\ORM\Query\Filter\Condition
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
Join::on("this.FILE_ID", "ref.ID")
->addCondition(new \Bitrix\Main\ORM\Query\Filter\Condition("ref.HEIGHT", "<=", 1000))
->addCondition(new \Bitrix\Main\ORM\Query\Filter\Condition("ref.WIDTH", "<=", 1000))
));
//LEFT JOIN b_file on this.FILE_ID = b_file.ID and b_file.HEIGHT <= 1000 and b_file.WIDTH <= 1000
То же самое, но только вместо where используется addCondition
и объект ConditionTree
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
Join::on("this.FILE_ID", "ref.ID")
->addCondition((new \Bitrix\Main\ORM\Query\Filter\ConditionTree())
->addCondition(new \Bitrix\Main\ORM\Query\Filter\Condition("ref.HEIGHT", "<=", 1000))
->addCondition(new \Bitrix\Main\ORM\Query\Filter\Condition("ref.WIDTH", "<=", 1000))
)
))
//LEFT JOIN b_file on this.FILE_ID = b_file.ID and (b_file.HEIGHT <= 1000 and b_file.WIDTH <= 1000)
То же самое, но смешиваем ConditionTree
c вызовами where
. Так получается короче
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
Join::on("this.FILE_ID", "ref.ID")
->addCondition((new \Bitrix\Main\ORM\Query\Filter\ConditionTree())
->where("ref.HEIGHT", "<=", 1000)
->where("ref.WIDTH", "<=", 1000)
)
))
//LEFT JOIN b_file on this.FILE_ID = b_file.ID and (b_file.HEIGHT <= 1000 and b_file.WIDTH <= 1000)
Чем может быть полезен ConditionTree
?
Например, можно добавить в группу условий or
вместо and
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
Join::on("this.FILE_ID", "ref.ID")
->addCondition((new \Bitrix\Main\ORM\Query\Filter\ConditionTree()
->where("ref.HEIGHT", "<=", 1000)
->where("ref.WIDTH", "<=", 1000)
->logic("or")//будем or между двумя условиями
)
));
//LEFT JOIN b_file on this.FILE_ID = b_file.ID and (b_file.HEIGHT <= 1000 or b_file.WIDTH <= 1000)
Так же можно добавить отрицание
перед группой условий
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
Join::on("this.FILE_ID", "ref.ID")
->addCondition((new \Bitrix\Main\ORM\Query\Filter\ConditionTree()
->where("ref.HEIGHT", "<=", 1000)
->where("ref.WIDTH", "<=", 1000)
->negative()//перед этой группой условий будет отрицание
)
));
//LEFT JOIN b_file on this.FILE_ID = b_file.ID and not (b_file.HEIGHT <= 1000 and b_file.WIDTH <= 1000)
Так же можно вообще не указывать Join::on
, а сразу описать нужные условия в ConditionTree
(new Reference(
"FILE",
\Bitrix\Main\FileTable::class,
(new \Bitrix\Main\ORM\Query\Filter\ConditionTree())
->where("this.FILE_ID", "=", ref.ID")
->where("ref.HEIGHT", "<=", 1000)
->where("ref.WIDTH", "<=", 1000)
));
//LEFT JOIN b_file on this.FILE_ID = b_file.ID and b_file.HEIGHT <= 1000 and b_file.WIDTH <= 1000)
В конце, хочу привести пример сложного запроса, который можно создать. Он взят из кода модуля forum
.
В данном примере с помощью метода whereColumn
добавляются условия ссылающиеся на другие RuntimeField
$query = MessageTable::query()
->setSelect(['ID'])
->where('TOPIC_ID', $topic->getId())
->registerRuntimeField('FORCED_INT_ID', new Main\Entity\ExpressionField('FORCED_ID', '%s + ""', ['ID']))
->registerRuntimeField(
'USER_TOPIC',
new Main\ORM\Fields\Relations\Reference(
'USER_TOPIC',
UserTopicTable::getEntity(),
[
'=this.TOPIC_ID' => 'ref.TOPIC_ID',
'=ref.USER_ID' => ['?i', $this->getId()],
],
['join_type' => 'LEFT']
)
)
->registerRuntimeField(
'USER_FORUM',
new Main\ORM\Fields\Relations\Reference(
'USER_FORUM',
UserForumTable::getEntity(),
[
'=this.FORUM_ID' => 'ref.FORUM_ID',
'=ref.USER_ID' => ['?i', $this->getId()]
],
['join_type' => 'LEFT']
)
)
->registerRuntimeField(
'USER_FORUM_0',
new Main\ORM\Fields\Relations\Reference(
'FUF0',
UserForumTable::getEntity(),
[
'=this.FORUM_ID' => ['?i', 0],
'=ref.USER_ID' => ['?i', $this->getId()]
],
['join_type' => 'LEFT']
)
)
->where(
Main\ORM\Query\Query::filter()
->logic('or')
->where(
Main\ORM\Query\Query::filter()
->whereNotNull('USER_TOPIC.LAST_VISIT')
->whereColumn('POST_DATE', '>', 'USER_TOPIC.LAST_VISIT')
)
->where(
Main\ORM\Query\Query::filter()
->whereNull('USER_TOPIC.LAST_VISIT')
->whereColumn('POST_DATE', '>', 'USER_FORUM.LAST_VISIT')
)
->where(
Main\ORM\Query\Query::filter()
->whereNull('USER_TOPIC.LAST_VISIT')
->whereNull('USER_FORUM.LAST_VISIT')
->whereColumn('POST_DATE', '>', 'USER_FORUM_0.LAST_VISIT')
)
->where(
Main\ORM\Query\Query::filter()
->whereNull('USER_TOPIC.LAST_VISIT')
->whereNull('USER_FORUM.LAST_VISIT')
->whereNull('USER_FORUM_0.LAST_VISIT')
->whereNotNull('ID')
)
)
->setOrder(['FORCED_INT_ID' => 'ASC'])
->setLimit(1)