Реализация ORM в ядре D7 — очередная интересная, перспективная, но как обычно плохо документированная разработка от 1с-Битрикс :) Призвана она абстрагировать разработчика от механики работы с таблицами на уровне запросов к БД, введя понятие сущности и поля сущности. На зимней партнерской конференции Алексей Кирсанов провел мастер-класс по созданию модуля с применением ORM для выборки данных из своей таблицы. После чего я решил провести небольшое исследование на предмет возможности построения более сложных, чем в примере, запросов.
Интересовало меня как общее устройство системы и принцип постороения кода, так и частные случаи запросов. Например, можно ли с помощью ORM использовать HAVING, DISTINCT и разные типы JOIN’ов. Забегая вперед, получилось далеко не все, но в общем конструктор эффективен и относительно прост.
Для начала изучаем понятия сущности, поля сущности и датаменеджера в документации. По-простому, сущность в терминах битрикса — это таблица, поля сущности — столбцы или «ссылки» на другие сущности, а датаменеджер — система управления данными. Для каждой сущности нужно создать описание, например
class ElementTable extends Entity\DataManager
{
public static function getFilePath()
{
return __FILE__;
}
public static function getTableName()
{
return 'b_iblock_element';
}
public static function getMap()
{
return array(
'ID' => array(
'data_type' => 'integer',
'primary' => true,
'autocomplete' => true,
'title' => Loc::getMessage('IBLOCK_ELEMENT_ENTITY_ID_FIELD'),
),
'IBLOCK_ID' => array(
'data_type' => 'integer',
),
'IBLOCK' => array(
'data_type' => 'Iblock',
'reference' => array('=this.IBLOCK_ID' => 'ref.ID'),
),
...
В getMap перечислены все поля таблицы, включая описание связей с другими сущностями. В примере таким образом указано отношение столбца IBLOCK_ID текущей таблицы и столбца ID сущности Iblock. В дальнейшем по reference-полям возможно выбирать поля связанных сущностей и использовать их в фильтрах.
Лайвхак: автоматически сгенерировать класс с описанием любой таблицы можно на странице Производительность-Таблицы, добавив параметр &orm=y в адрес.
Тренироваться будем на таблицах типов инфоблока, самих инфоблоков и элементов. Для них описаны сущности TypeTable, IblockTable и ElementTable, их можно посмотреть в исходниках модуля iblock. Операции добавления, удаления и выборки данных по первичному ключу (getById) нас не интересуют, будем строить обычные getList-запросы произвольного вида через цепочку вызовов методов класса Query.
Пример #1
Выберем 10 первых элементов из инфоблока ID=1.
\Bitrix\Main\Loader::IncludeModule("iblock");
// создаем объект Query. В качестве параметра он принимает объект сущности, относительно которой мы строим запрос
$query = new \Bitrix\Main\Entity\Query(Bitrix\Iblock\ElementTable::getEntity());
// можно еще так: :)
// $query = new \Bitrix\Main\Entity\Query(Bitrix\Main\Entity\Base::getInstance("Bitrix\Iblock\ElementTable"));
$query
->setSelect(array("ID", "NAME"))
->setFilter(array("IBLOCK_ID" => 1))
->setOrder(array("ID" => "ASC"))
->setLimit(10);
$query->exec();
$query->dump();
Пример #2
Выберем элементы инфоблока с группировкой по названию. Стоит отметить, что битрикс автоматически добавляет в число полей для группировки все, что указано в setSelect.
$query
->setSelect(array("NAME"))
->setFilter(array("IBLOCK_ID" => 1))
->setGroup(array("NAME"))
Пример #3
В запросах можно использовать агрегатные функции mysql. Для это служит метод registerRuntimeField, регистрирующий новое поле на время выполнения запроса.
Подсчитаем количество элеметов с группировкой по имени.
$query
// cnt - название поля
->registerRuntimeField("cnt", array(
// тип вычисляемого поля
"data_type" => "integer",
// агрегатная функция (count, max, sum, avg...) и поле для подстановки
"expression" => array("count(%s)", "NAME")
)
)
->setSelect(array("NAME", "cnt"))
->setFilter(array("IBLOCK_ID" => 1))
->setGroup(array("NAME"));
Пример #4
Допустим, нам нужно в предыдущем примере ограничить выборку только теми названиями, которые встречаются более 5 раз. Написав фильтр по агрегатному runtime-полю, битрикс автоматически переносит его в условие having! Причем в параметрах группировки не обязательно указывать поле, к которому применяется агрегатная функция.
$query
->registerRuntimeField("cnt", array(
"data_type" => "integer",
"expression" => array("count(%s)", "NAME")
)
)
->setSelect(array("NAME", "cnt"))
->setFilter(array("IBLOCK_ID" => 1, ">cnt" => 5));
Будет сгенерирован запрос вида:
select NAME, count(NAME) as cnt from table where IBLOCK_ID=1 group by NAME having cnt>5
Пример #5
Через сущность «элемент» можно выбирать или ставить условия на поля связанной сущности «инфоблок». Связанная таблица по умолчанию присоединяется с помощью left join. Вспомним reference-поле IBLOCK в описании ElementTable и выберем название инфоблока с ID=1 через таблицу с элементами:
$query
->setSelect(array("IBLOCK.NAME"))
->setFilter(array("IBLOCK.ID" => 1))
// или
//->setFilter(array("IBLOCK_ID" => 1))
->setLimit(1);
Запрос:
SELECT `iblock_element_iblock`.`NAME` AS `IBLOCK_ELEMENT_IBLOCK_NAME` FROM `b_iblock_element` `iblock_element` LEFT JOIN `b_iblock` `iblock_element_iblock` ON `iblock_element`.`IBLOCK_ID` = `iblock_element_iblock`.`ID` WHERE `iblock_element_iblock`.`ID` = 1 LIMIT 0, 1
Пример #6
Runtime- может быть не только вычисляемое поле, но и ссылка на другую сущность. Т.е. в функции getMap мы можем не описывать связь, а сформировать ее прямо в запросе. Например, создадим объект Query для сущности IblockTable, свяжем ее с ElementTable и выберем элемент с ID=1:
// Query(IblockTable)
$query = new \Bitrix\Main\Entity\Query(Bitrix\Iblock\IblockTable::getEntity());
$query
// поле element как ссылка на таблицу b_iblock_element
->registerRuntimeField("element", array(
// тип - сущность ElementTable
"data_type" => "Bitrix\Iblock\ElementTable",
// обратите внимание, что this.ID относится к таблице, относительно которой строится запрос
// т.е. b_iblock.ID = b_iblock_element.IBLOCK_ID
'reference' => array('=this.ID' => 'ref.IBLOCK_ID'),
)
)
// все поля элемента и название инфоблока
->setSelect(array("element", "NAME"))
->setFilter(array("element.ID" => 1));
Небольшая особенность — в setSelect нужно обязательно указывать runtime-поле, что добавляет в select все поля связанной таблицы.
Пример #7
В определении runtime-reference-поля можно указывать тип join’a, а в фильтре использовать сложную логику как в CIblockElement::GetList():
$query = new \Bitrix\Main\Entity\Query(Bitrix\Iblock\IblockTable::getEntity());
$query
->registerRuntimeField("element", array(
"data_type" => "Bitrix\Iblock\ElementTable",
'reference' => array('=this.ID' => 'ref.IBLOCK_ID'),
'join_type' => "LEFT"
)
)
->registerRuntimeField("type", array(
"data_type" => "Bitrix\Iblock\TypeTable",
'reference' => array('=this.IBLOCK_TYPE_ID' => 'ref.ID'),
'join_type' => "RIGHT"
)
)
->setSelect(array("element", "type"))
->setFilter(array(
"LOGIC" => "OR",
array("element.ID" => 1),
array("ID" => 3)
)
)
->setLimit(2);
Пример #8
Если несколько таблиц в описании сущностей (не через runtime-поля!) связаны по цепочке, то их можно использовать в запросе через символ «.». Например, выберем ID типа инфоблока для элемента с ID=1:
$query = new \Bitrix\Main\Entity\Query(Bitrix\Iblock\ElementTable::getEntity());
$query
// b_iblock_element.IBLOCK_ID = b_iblock.TYPE_ID = b_iblock_type.ID
->setSelect(array("IBLOCK.TYPE.ID"))
->setFilter(array("ID" => 1));
К сожалению, не получилось создать запрос, на который повлиял бы метод enableDataDoubling и выяснить, что можно передавать в метод setOptions. У кого есть мысли на этот счет, прошу в комментарии.
Напоследок привет от битрикса :)
/*
/bitrix/modules/main/lib/entity/query.php
* The most magic method. Do not edit without strong need, and for sure run tests after.
*/
protected function collectExprChains(QueryChain $chain, $storages = array('hidden'))
Больше статей и материалов по web-разработке в tg-канале - подписывайтесь!
Подписаться в telegram
Отличная статья, было бы не плохо добавить пример с фильтрацией по свойствам элементам, я считаю это актуально пример
Максим, свойства элементов инфоблока на момент написания статьи еще были не реализованы в классах сущностей, поэтому соответствующих примеров добавить не получилось.
А че им DQL не нравится? Как раз для любителей сферических абстрактных коней в вакууме с напухшими вишнями, чтобы детей по ночам пугать :)
Это уже не ПэХаПэ — это уже перловые лиспы и не иначе :D
Кроме шуток: что в реальности это дает на практике программисту кроме бумажки в рамочке и умного вида?
Дмитрий, ORM в общем — это круто. ORM в Битриксе — это говно. Я не понимаю, про какие бумажки ты сейчас говоришь, но ООП в умелых руках помогает писать гибкие и расширяемые приложения, внося минимальные изменения в функционал. Это возможно благодаря наследованию. Это не ваши ссаные повсюду объявленые функции и скопированный во все места одинаковый код.
Оооххх … я боюсь даже подумать о таких людях, которые сами пишут ОРМ c мутабельным ООП под свои разработки. Чаще все на самописах пытаются и там все … Ну мягко говоря не до таких расширений сознания, которые может потенциально принести лавандосом потребитель, не говоря про нагрузку.
Жизненно, но непотребно из-за 15 возможностей наворачивать обертки классов и говорить по полиморфизм.
А вообще да. Но это снова выход на урвень туповатого чмощника который сидит в визивиге и не понимает зачем ему те или иные инстансы :D
Имхо.
как можно добавить DISTINCT в select ?
Алексей, подскажите, пожалуйста, а с админкой для сrm-сущностей вы уже делали? интересует спискок записей и страница детальной записи… с возможностью из редактирования и удаления
Уберите из описания DISTINCT чтобы людей не обманывать)
DISTINCT запросы делать нельзя.
DISTINCT можно делать через Bitrix\Main\Entity\ExpressionField.
Столкнулся с тем что было необходимо через ORM написать запрос MATCH AGAINST. Вот пример реализации:
$myIterator = MyTable::getList(array(
‘runtime’ => array(
new Entity\ExpressionField(‘1’, ‘1’),
),
‘filter’ => array(
‘=1’ => new DB\SqlExpression(‘1 AND MATCH(a,b) AGAINST (?s)’, $query)
)
));
где $query — искомая строка, a,b — fulltext индекс таблицы.
Забавно, ТП сказала что анреал сделать)
http://i.imgur.com/GGWKBCU.png
Можно небольшой пример?)
https://webdevprompt.com/2021/07/28/bitrix-orm-object-relational-mapping/ если кому интересно, обвноленная ORM D7
Если требуется select FIELD as NAME, то можно так :
$query->setSelect([‘NAME’ => ‘FIELD’]);