Глубокое погружение в ORM. От основ до решения нетиповых проблем и новинок
ORM — Object-Relational Mapping, что в переводе «объектно-реляционное отображение».
Суть технологии заключается в описании таблицы для доступа к ней с помощью средств ООП
Имеет достаточно гибкий функционал для работы с сущностями, их генерацией налету, именованными методами, наличием обёрток и помощников для построения запросов
Также поставляется с большим количеством уже готовых сущностей модулей ядра
https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&CHAPTER_ID=05748
С версии ядра 12.0.0 (2012-13 год) появился абстрактный базовый класс для работы с объектами данных DataManager (исходя из офф. документации по D7).
С версии ядра 18.0.3 (2018-06-07) добавляются объекты ORM, в это же время меняется структура классов ORM (исходя из описания истории версий).
С версии ядра 19.0.0 (2020 год) у модуля iblock добавлена поддержка ORM при работе с элементами инфоблоков.
В 2012 году использование данной технологии стало маст-хэвом.
С конца 2019 года уходим от массивов и описание таблиц вручную и применяем объекты.
По определению Bitrix Сущность — это совокупность коллекции объектов с присущей им базовой (низкоуровневой) бизнес-логикой.
Все сущности являются потомками класса
\Bitrix\Main\ORM\Data\DataManager
Например, сущность Пользователь — множество пользователей с присущим набором полей: ID, LOGIN, EMAIL, PASSWORD и т.д.
В парадигме Bitrix под сущностью подразумевается User, но класс с её описанием должен иметь постфикс Table, то есть UserTable
Основные имена были зарезервированы
Но сегодня в работе с ORM мы получим объект класса с префиксом EO_, то есть EO_User
Обращение через физически описанный класс
Наследуется от \Bitrix\Main\ORM\Data\DataManager и обязательно определяет методы getTableName и getMap
Первый возвращает название таблицы в БД. Второй — массив полей сущности
Наследуется от \Bitrix\Highloadblock\DataManager который расширяет \Bitrix\Main\ORM\Data\DataManager
Стоит определить метод getHighloadBlock который возвращает массив данных о highload-блоке
Вручную описывать сущности Highload-блоков нет необходимости, поскольку для них существует компиляция. Делать это нужно в случаях если:
При генерации аннотаций модуля highloadblock создается описание только для таблиц модуля, а не для таблиц пользовательских highload-блоков
Все поля являются потомками класса \Bitrix\Main\ORM\Fields\Field
На текущий момент их более двадцати
abstract class
ScalarField
extends Field implements IStorable, ITypeHintable
abstract class
Relation
extends Field implements ITypeHintable
Важно знать о возможностях поля. Оно позволяет не хранить данные в таблице, а зарегистрировать виртуальное поле базирующиеся на SQL выражении
new ExpressionField(
'SRC_PREVIEW_URL',
'CONCAT("/upload/", %s, \'/\', %s)',
[
'SRC_PREVIEW_FILE.SUBDIR',
'SRC_PREVIEW_FILE.FILE_NAME',
]
)
Это сущность описание которой выполняется непосредственно в момент вызова определенного метода
Есть ряд готовых методов, которые позволяют компилировать разные по специфике сущности
$data = HighloadBlockTable::getById(int: id)->fetch()
$data = HighloadBlockTable::getList(array: parameters)->fetch()
Сущность компилируемая на лету без предварительного вызова каких либо методов
Обращение через имя Element{API_CODE}Table
На данный момент возможно только для сущности элементов инфоблока — таблица b_iblock_element. Для работы необходим заполненный ApiCode инфоблока
Физически большинства методов и таблиц нет, поэтому IDE не сможет вам построить подсказки
Для быстрой и удобной работы в IDE нужно сгенерировать аннотации
Установка зависимостей composer
cd bitrix && COMPOSER=composer-bx.json composer install
Можно подключить в свой composer.json помощью merge-plugin
Подробнее в документации https://github.com/medveddev/bxorm/blob/master/composer.md
# Базовая команда (модуль main)
php bitrix.php orm:annotate
# Чтение всех модулей
php bitrix.php orm:annotate -m all
# Чтение нужных модулей
php bitrix.php orm:annotate -m main,iblock,webpractik.main
# Дополнение аннотаций
php bitrix.php orm:annotate -m catalog
# Перезапись аннотаций
php bitrix.php orm:annotate -c -m catalog
С версии v20.100.0 главного модуля аннотации основных сущностей включены в поставку
В будущем обещали добавить режим разработки, где они будут обновляться в реальном времени
Подробнее в документации https://github.com/medveddev/bxorm/blob/master/90_annotate.md
# Получение объекта запроса
ElementTable::query();
# Создание объекта элемента таблицы
ElementTable::createObject();
# Создание объекта коллекции элементов
ElementTable::createCollection();
# Получение готовой сущности
ElementTable::getEntity();
Получение массива полей сущности
ElementTable::getMap();
Восстановление объекта
ElementTable::wakeUpObject(mixed: $row);
Восстановление коллекции
ElementTable::wakeUpCollection(mixed: $rows);
# Удаление элемента
ElementTable::delete(mixed: $primary);
Если есть доступ к объекту элемента, то нужно вызвать его метод $object->delete();
# Обновление элемента
ElementTable::update(mixed: $primary, array: $data);
Если есть доступ к объекту элемента, то нужно обновлять его поля с сохранением $object->setName('BlaBla')->save();
# Добавление элемента
ElementTable::add(array: $data);
Для добавления стоит использовать конструкцию вида:
ElementTable::createObject()->setName('BlaBla')->save();
$item1 = ElementTable::createObject()->setName('BlaBla');
$item2 = ElementTable::createObject()->setName('BlaBla');
$collection = ElementTable::createCollection();
$collection->add($item1);
$collection->add($item2);
$collection->save();
# Запрос по параметрам
ElementTable::getList(array: $parameters = []);
# Получение элемента по параметрам
ElementTable::getByPrimary(mixed: $primary, array: $parameters = []);
# Получение элемента по ID
ElementTable::getById(mixed: $id);
Обращение к полям связей происходит через точку
ElementTable::query()
->setSelect(['NAME', 'CITY.NAME']) // Плохо
->addSelect('NAME')
->addSelect('CITY.NAME');
registerRuntimeField(object: Field)
registerRuntimeField(new Reference(
'REF_CITY',
CityTable::class,
Join::on('this.CITY.VALUE', 'ref.ID')
))
registerRuntimeField(string: name, array fieldInfo) # Плохо
registerRuntimeField('REF_CITY', [
'data_type' => CityTable::class,
'reference' => ['=this.CITY.VALUE' => 'ref.ID'],
'join_type' => 'LEFT',
])
Данный вариант менее предпочтительный, чем добавление поля в описание сущности
Придется дублировать код описания при каждой выборке, также к нему нельзя обратиться через магический get{Name}
ElementTable::query()
->registerRuntimeField(new Reference(
'REF_CITY',
CityTable::class,
Join::on('this.CITY.VALUE', 'ref.ID')
))
->addSelect(new Reference(
'REF_CITY',
CityTable::class,
Join::on('this.CITY.VALUE', 'ref.ID')
));
ElementTable::query()
->setFilter(['=ID' => 10])
->addFilter('=ID', 10);
Помимо where есть заготовленные методы для разных операторов фильтра. Также для каждого из типов фильтра есть отрицательный вариант whereNot или
where{Operator}Not
Мало кто знает, но whereColumn('FIELD_1', 'FIELD_2') является оберткой над where('FIELD_1', new \Query\Filter\Expression\Column('FIELD_2'))
Зная это можно быстро составлять выражения равенства между колонками
whereIn('LOGIN', [
new Column('NAME'),
new Column('LAST_NAME')
])
Все добавленные фильтры в query имеют логику AND
Если нужно сделать более сложные или вложенные правила, то можно в where передать объект фильтра
use \Bitrix\Iblock\ORM\Query;
$filter = Query::filter()
->logic(Query::filter()::LOGIC_OR)
->where('CODE', 'test')
->where('CODE', 'test_test')
->whereNull('CODE');
// Сделать все условия фильтра негативными
// $filter->negative();
ElementTable::query()
->whereNotNull('CITY')
->whereBetween('ID', 1, 100)
->whereIn('USER.LOGIN', [
new Column('USER.NAME'),
new Column('USER.LAST_NAME')
])
->where($filter);
Если понадобится список операторов фильтра можно посмотреть тут:
\Bitrix\Main\ORM\Query\Filter\Operator::get()
CAllSQLWhere::$operations
CAllIBlock::MkOperationFilter()
На крайний случая можно заглянуть в этот метод (он парсит строки старого фильтра вgetList)
Нужно ли использовать exec() перед fetchCollection() или fetchObject()?
Если для рабочей сущности были заранее сгенерированы аннотации, то exec можно опустить. В противном случае его обязательно нужно указывать, для корректной работы подсказок IDE
$query->fetchCollection(); |
$query->exec()->fetchCollection(); |
$query->fetchObject(); |
$query->exec()->fetchObject(); |
// Bitrix\Main\ORM\Query\Query
/**
* Short alias for $result->fetchCollection()
*
* @return null Actual type should be annotated by orm:annotate
* @throws Main\ObjectPropertyException
* @throws Main\SystemException
*/
public function fetchCollection()
{
return $this->exec()->fetchCollection();
}
Установка значения
Сброс установленного значения
Удаление значения (словно не добавляли select)
При наличии аннотаций доступны магические аналоги конкретных полей
Получение значения
Обязательное получение значения. В случае его отсутствия выбрасывается SystemException
Получение актуального значения для БД,а не установленного в процессе
При наличии аннотаций доступны магические аналоги конкретных полей
Если нужно быстро получить массив значений всех полей, то можно воспользоваться методом collectValues()
Особенно удобно, когда в выборке только скалярные поля. Дабы код был типизирован и читаем следует использовать только где это правда необходимо, а не всегда!)
Добавление значения где value — это ключ или объект
Удаление значения где value — это ключ или объект
Удаление всех значений
При наличии аннотаций доступны магические аналоги конкретных полей
Содержит ли актуальное значение из БД
Было ли установлено новое значение
Это isFilled() || isChanged()
При наличии аннотаций доступны магические аналоги конкретных полей
Объект сущности. Полезно получения списка полей / свойств и данных инфоблока
Объект \Bitrix\Main\Type\Dictionary. Полезно для хранения своих данных привязанных к объекту элемента. Имеет методы set и get
Сохранение всех изменений объекта в БД
Удаление элемента из БД
Делает подзапрос для заполнение недостающий полей объекта. Где value — это поле, массив полей или маска полей
# Возможные маски полей
# \Bitrix\Main\ORM\Fields\FieldTypeMask
FieldTypeMask::SCALAR
FieldTypeMask::EXPRESSION
FieldTypeMask::USERTYPE
FieldTypeMask::REFERENCE
FieldTypeMask::ONE_TO_MANY
FieldTypeMask::MANY_TO_MANY
FieldTypeMask::FLAT
FieldTypeMask::RELATION
FieldTypeMask::ALL
Класс коллекции реализует интерфейсы ArrayAccess, Iterator, Countable
Для доступа к элементам можно перебирать коллекцию в цикле или же использовать метод getAll(), который вернет содержащиеся в ней объекты
Добавление элемента в коллекцию
Удаление элемента из коллекции
Наличие конкретного объекта в коллекции
Аналогично предыдущему слайду, но в параметра передается primary объекта
Во избежании лишних итераций циклов можно получать данные прямо из коллекции. Для этого есть два метода:
Получает массив значений поля. Можно использовать как для скалярных полей, так и для связей
$collection->getIdList()
Получает массив значений поля. Можно использовать только для связей
$collection->getCityList()
$collection->getCityCollection()
Объект сущности. Полезно получения списка полей / свойств и данных инфоблока
Делает подзапрос для заполнение недостающий полей коллекции. Где value — это поле, массив полей или маска полей
Сохранение изменений коллекции в БД
Далее будут примеры работы с несколькими инфоблоками
Построить отображение имен жителей по 5 с пагинацией на ORM
Создаем и заполняем объект пагинации
\Bitrix\Main\UI\PageNavigation
$nav = new PageNavigation('people-nav');
$nav->allowAllRecords(true);
$nav->setPageSize(5);
$nav->initFromUri();
Формируем выборку
$query = ElementPeopleTable::query()
->addSelect('ID')
->addSelect('NAME')
->setLimit($nav->getLimit())
->setOffset($nav->getOffset())
->countTotal(true);
$exec = $query->exec();
$collection = $exec->fetchCollection();
$nav->setRecordCount($exec->getCount());
Вывод имен
foreach ($collection as $item) {
echo $item->getName() . '<br>';
}
Вывод пагинации. Для этого есть готовый компонент в параметры которого нужно передать объект пагинации (сформированный ранее)
$APPLICATION->IncludeComponent(
'bitrix:main.pagenavigation',
'',
[
'NAV_OBJECT' => $nav,
'SEF_MODE' => 'N',
],
false
);
use Bitrix\Iblock\Elements\ElementPeopleTable;
use Bitrix\Main\UI\PageNavigation;
require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/header.php');
$nav = new PageNavigation('people-nav');
$nav->allowAllRecords(true)
->setPageSize(5)
->initFromUri();
$query = ElementPeopleTable::query()
->addSelect('ID')
->addSelect('NAME')
->setLimit($nav->getLimit())
->setOffset($nav->getOffset())
->countTotal(true);
$exec = $query->exec();
$collection = $exec->fetchCollection();
$nav->setRecordCount($exec->getCount());
foreach ($collection as $item) {
echo $item->getName() . '<br>';
}
$APPLICATION->IncludeComponent(
'bitrix:main.pagenavigation',
'',
[
'NAV_OBJECT' => $nav,
'SEF_MODE' => 'N',
],
false
);
require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/footer.php');
Добавить к предыдущим условиям отображение домов жителей (множественное свойство)
Если используется лимит в запросе, то множественные поля нельзя добавлять в выборку — результат будет некорректный
Дело в том, что на уровне sql добавленное в выборку множественное поле это отдельная запись. Лимит сработает на количество записей, а не на количество уникальных элементов в запросе
Решение — поместить все множественные поля в fill() коллекции
Заполняем недостающие данные методом fill()
$query = ElementPeopleTable::query()
->addSelect('ID')
->addSelect('NAME')
->setLimit($nav->getLimit())
->setOffset($nav->getOffset())
->countTotal(true);
$exec = $query->exec();
$collection = $exec->fetchCollection();
$collection->fill(['HOME.ELEMENT']);
$nav->setRecordCount($exec->getCount());
Правим цикл
foreach ($collection as $item) {
/** @var EO_ElementHome_Collection $homes */
$homes = $item->getHome()->getElementCollection();
$name = 'Житель: ' . $item->getName() . ';<br>';
$home = 'Дома: ' . implode('; ', $homes->getNameList()) . '<br>';
echo $name . $home . '<br>';
}
Добавить к предыдущим условиям отображение названия города
Самый поверхностный вариант — это делать подзапрос в другую таблицу по полученному ID дома. Нежизнеспособный вариант и вот почему:
Правильный вариант — это добавление новых связей для получениях их в одном запросе
Самый правильный вариант — это модификация сущности, чтобы эти поля были доступны по всему коду проекта
Пока пойдем по первому варианту
Пагинация
$nav = new PageNavigation('people-nav');
$nav->allowAllRecords(true)
->setPageSize(5)
->initFromUri();
Получаем объект таблицы и сущности
$table = new ElementPeopleTable();
$entity = $table::getEntity();
Получаем нужное поле сущности, помогаем IDE, указывая её реальный класс, и добавляем необходимую связь
/** @var PropertyOneToMany $homeProperty */
$homeProperty = $entity->getField('HOME');
$homeProperty->getRefEntity()->addField(new Reference(
'ITEM',
ElementHomeTable::getEntity(),
Join::on('this.VALUE', 'ref.ID')
));
Далее обращаемся к таблице для построения запроса
$query = $table::query()
->addSelect('ID')
->addSelect('NAME')
->setLimit($nav->getLimit())
->setOffset($nav->getOffset())
->countTotal(true);
$exec = $query->exec();
$collection = $exec->fetchCollection();
$collection->fill(['HOME.ITEM.CITY.ELEMENT.NAME']);
$nav->setRecordCount($exec->getCount());
Правим работу с данными в цикле
foreach ($collection as $item) {
$cityNames = [];
foreach ($item->getHome() as $home) {
$cityNames[] = $home->get('ITEM')->get('CITY')->get('ELEMENT')->get('NAME');
}
$name = 'Житель: ' . $item->getName() . '<br>';
$home = 'Дома: ' . implode('; ', $cityNames) . '<br>';
echo $name . $home . '<br>';
}
Вывод пагинации
$APPLICATION->IncludeComponent(
'bitrix:main.pagenavigation',
'',
[
'NAV_OBJECT' => $nav,
'SEF_MODE' => 'N',
],
false
);
Стандартную сущность можно и нужно расширять путем наследования, а как быть с динамическими сущностями?
Ответ: никак. При попытке наследования от такой сущности вы получите ошибку. Есть только косвенное решение
Как пример можно рассмотреть расширение таблицы свойств
Добавим возможность получить коллекцию значений свойства
Наследуемcя с определением getMap(). В данном случае можно getTableName() — реализован у родителя
class PropertyTable extends \Bitrix\Iblock\PropertyTable
{
public static function getMap(): array {...}
}
Реализация getMap() расширенной сущности PropertyTable. В данном случае ElementPropertyTable — это также расширенная версия и
PROPERTY указывает на поле типа Reference
public static function getMap(): array
{
$map = parent::getMap();
$map['VALUES'] = new OneToMany(
'VALUES',
ElementPropertyTable::class,
'PROPERTY'
);
return $map;
}
Наследуемcя с определением обоих методов getTableName() и getMap()
class ElementPropertyTable extends \Bitrix\Iblock\ElementPropertyTable
{
public static function getTableName(): string{...}
public static function getMap(): array {...}
}
Реализация getTableName() расширенной сущности ElementPropertyTable
public static function getTableName(): string
{
return 'b_iblock_element_property';
}
Реализация getMap() расширенной сущности ElementPropertyTable. Тут новое поле не только в роли новой связи, но и в роли ссылки для сущности
PropertyTable (для построения множественной связи)
public static function getMap(): array
{
$map = parent::getMap();
$map['PROPERTY'] = new Reference(
'PROPERTY',
PropertyTable::getEntity(),
Join::on('this.IBLOCK_PROPERTY_ID', 'ref.ID')
);
return $map;
}
Есть идеи как это сделать?
Поскольку наследование от динамической сущности невозможно, то стоит искать обходной путь
Суть заключается в тех же самых действиях, что в задаче №3 — сначала модификация сгенерированной сущности, а затем выборка
Пример на основе структуры инфоблоков описанных ранее для задач
Создаем абстрактный класс TableConfigurator
abstract class TableConfigurator
{
private static $tables;
private static function getInstance() {...}
abstract protected static function configure(): void;
abstract protected static function getTableClass(): string;
public static function build() {...}
}
Закрытое свойство $tables хранит сконфигурированные экземпляры таблиц
private static $tables;
Закрытый статичный метод getInstance() возвращает существующий или создает новый экземпляр таблицы
private static function getInstance()
{
if (isset(self::$tables[static::class])) {
return self::$tables[static::class];
} else {
$tableClass = static::getTableClass();
self::$tables[static::class] = new $tableClass();
static::configure();
return self::$tables[static::class];
}
}
Публичный метод build() возвращает экземпляр таблицы
public static function build()
{
return static::getInstance();
}
Абстрактный защищенный метод configure() — это основной метод конфигурации сущности
abstract protected static function configure(): void;
Абстрактный защищенный метод getTableClass() — это метод возвращающий класс конфигурируемой таблицы
abstract protected static function getTableClass(): string;
Сущность житель
final class PeopleTable extends TableConfigurator
{
public static function build(): ElementPeopleTable
{
return parent::build();
}
protected static function configure(): void
{
self::configureHomeProperty();
}
protected static function getTableClass(): string
{
return ElementPeopleTable::class;
}
protected static function configureHomeProperty(): void
{
$entity = self::build()::getEntity();
/** @var PropertyOneToMany $field */
$field = $entity->getField('HOME');
$refEntity = $field->getRefEntity();
$refEntity->addField(new Reference(
'ITEM',
HomeTable::build()::getEntity(),
Join::on('this.VALUE', 'ref.ID')
));
}
}
Сущность дом
final class HomeTable extends TableConfigurator
{
public static function build(): ElementHomeTable
{
return parent::build();
}
protected static function configure(): void
{
self::configureStreetProperty();
self::configureCityProperty();
}
protected static function getTableClass(): string
{
return ElementHomeTable::class;
}
protected static function configureStreetProperty(): void
{
$entity = self::build()::getEntity();
/** @var PropertyReference $field */
$field = $entity->getField('STREET');
$refEntity = $field->getRefEntity();
$refEntity->addField(new Reference(
'ITEM',
StreetTable::build()::getEntity(),
Join::on('this.VALUE', 'ref.ID')
));
}
protected static function configureCityProperty(): void
{
$entity = self::build()::getEntity();
/** @var PropertyReference $field */
$field = $entity->getField('CITY');
$refEntity = $field->getRefEntity();
$refEntity->addField(new Reference(
'ITEM',
ElementCityTable::getEntity(),
Join::on('this.VALUE', 'ref.ID')
));
}
}
Сущность улица
final class StreetTable extends TableConfigurator
{
public static function build(): ElementStreetTable
{
return parent::build();
}
protected static function configure(): void
{
self::configureCityProperty();
}
protected static function getTableClass(): string
{
return ElementStreetTable::class;
}
protected static function configureCityProperty(): void
{
$entity = self::build()::getEntity();
/** @var PropertyReference $field */
$field = $entity->getField('CITY');
$refEntity = $field->getRefEntity();
$refEntity->addField(new Reference(
'ITEM',
ElementCityTable::getEntity(),
Join::on('this.VALUE', 'ref.ID')
));
}
}
// Формирование объекта пагинации
$query = PeopleTable::build()::query()
->addSelect('ID')
->addSelect('NAME')
->setLimit($nav->getLimit())
->setOffset($nav->getOffset())
->countTotal(true);
$exec = $query->exec();
$collection = $exec->fetchCollection();
$nav->setRecordCount($exec->getCount());
$collection->fill([
'HOME.ITEM.NAME',
'HOME.ITEM.STREET.ITEM.NAME',
'HOME.ITEM.CITY.ITEM.NAME',
]);
foreach ($collection as $item) {
$addresses = [];
foreach ($item->getHome() as $home) {
$homeName = $home->get('ITEM')->getName();
$streetName = $home->get('ITEM')->get('STREET')->get('ITEM')->getName();
$cityName = $home->get('ITEM')->get('CITY')->get('ITEM')->getName();
$addresses[] = "д.$homeName ул.$streetName г.$cityName";
}
$name = 'Житель: ' . $item->getName() . '<br>';
$home = implode('<br>', $addresses) . '<br>';
echo $name . $home . '<br>';
}
// Вызов компонента пагинации
Плюсы
Минусы
DataManager и отсутствия метода getMap() нет возможности получить аннотации по добавленным полям
Сначала необходимо с помощь миграций добавить индекс полям
Метод up() миграции на laravel
DB::statement('CREATE fulltext index IXF_B_IBLOCK_ELEMENT_1 on b_iblock_element (NAME)');
DB::statement('CREATE fulltext index IXF_B_IBLOCK_ELEMENT_2 on b_iblock_element (PREVIEW_TEXT)');
Можно создать индекс для поля SEARCHABLE_CONTENT в которое по умолчанию пишутся поля NAME, PREVIEW_TEXT, DETAIL_TEXT
Метод down() миграции на laravel
Schema::table('b_iblock_element', function ($table) {
$table->dropIndex('IXF_B_IBLOCK_ELEMENT_1');
$table->dropIndex('IXF_B_IBLOCK_ELEMENT_2');
});
Для решения задач полнотекстового поиска в Query ORM добавили поддержку оператора match, а также помощник
whereMatch()
whereNotMatch()
Bitrix\Main\ORM\Query\Filter\Helper::matchAgainstWildcard()
Синтаксис оператора MATCH
MATCH (col1,col2,...) AGAINST (expr [search_modifier])
Где search_modifier — это модификатор поиска. Возможные значения: IN NATURAL LANGUAGE MODE, IN BOOLEAN MODE или
WITH QUERY EXPANSION. Подробнее можно прочесть, например, тут https://habr.com/ru/post/40218/
ORM работает только со вторым IN BOOLEAN MODE — это полнотекстовый поиск в логическом режиме с возможностью использовать специальные операторы
Пример запроса:
SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database' IN NATURAL LANGUAGE MODE);
Подробнее можно почитать в документации https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.html
Отсутствие оператора — это логическое ИЛИ
находит строки, содержащие по меньшей мере одно из этих слов
apple banana
Предшествующий слову знак + (плюс) показывает, что это слово должно присутствовать в каждой возвращенной строке
находит строки, содержащие оба слова
+apple +juice
находит строки, содержащие слово apple, но ранг строки выше, если она также содержит слово macintosh
+apple macintosh
Предшествующий слову знак - (минус) означает, что это слово не должно присутствовать в какой-либо возвращенной строке
находит строки, содержащие слово apple, но не macintosh
+apple -macintosh
Скобки ( ) группируют слова в подвыражения
Два оператора < > используются для того, чтобы изменить вклад слова в величину релевантности, которое приписывается строке. Оператор < уменьшает этот вклад, а оператор > — увеличивает его
находит строки, содержащие apple и pie, или apple и strudel (в любом порядке), но ранг apple pie выше, чем apple strudel
+apple +(>pie <strudel)
Предшествующий слову знак ~ (тильда) воздействует как оператор отрицания, обуславливая негативный вклад данного слова в релевантность строки. Им отмечают нежелательные слова. Строка, содержащая такое слово, будет оценена ниже других, но не будет исключена совершенно, как в случае оператора - (минус)
Символ * (звездочка) является оператором усечения. В отличие от остальных операторов, она должна добавляться в конце слова, а не в начале
находит строки, содержащие apple, apples, applesauce, или applet
apple*
Фраза, заключенная в " (двойные кавычки) , соответствует только строкам, содержащим эту фразу, написанную буквально
находит строки, содержащие some words of wisdom, но не some noise words
"some words"
При передачи в фильтр значения Олег Анатольевич без обработки будут найдены записи где есть хотя бы одно из слов
Запрос
$collection = PeopleTable::build()::query()
->addSelect('ID')
->addSelect('NAME')
->whereMatch('NAME', 'Олег Анатольевич')
->fetchCollection();
При передачи в фильтр значения Олег Анатольевич без обработки будут найдены записи где есть хотя бы одно из слов
Результат
SELECT `iblock_elements_element_people`.`ID` AS `ID`, `iblock_elements_element_people`.`NAME` AS `NAME` FROM `b_iblock_element` `iblock_elements_element_people` WHERE MATCH (`iblock_elements_element_people`.`NAME`) AGAINST ('Олег Анатольевич' IN BOOLEAN MODE) AND `iblock_elements_element_people`.`IBLOCK_ID` = 33
Голубев Олег Анатольевич
Шварценеггер Олег Петрович
Викторович Олег
Чтобы результат был более корректным нужно обработать значение для поиска. Можно использовать готовый помощник
Helper::matchAgainstWildcard('Олег Анатольевич','*'), где второй параметр
wildcard
либо звезда, либо пустая строка
wildcard добавляется в конце каждого слова
Результат с предварительной обработкой помощником
SELECT `iblock_elements_element_people`.`ID` AS `ID`, `iblock_elements_element_people`.`NAME` AS `NAME` FROM `b_iblock_element` `iblock_elements_element_people` WHERE MATCH (`iblock_elements_element_people`.`NAME`) AGAINST ('(+Олег* +Анатол*)' IN BOOLEAN MODE) AND `iblock_elements_element_people`.`IBLOCK_ID` = 33
Голубев Олег Анатольевич
Олегович Анатолий
Добавим элемент с именем равным значению поиска Олег Анатол и увидим, что он оказался в конце списка при использовании помощника
Голубев Олег Анатольевич
Олегович Анатольевичный
Олег Анатол
Это происходит из за использования операторов. Это можно подтвердить вернувшись к первому варианту без обработки значения помощником
Олег Анатол
Голубев Олег Анатольевич
Шварценеггер Олег Петрович
Викторович Олег
Чтобы управлять процессом поиска мы должны самостоятельно обработать поисковую фразу. В моих тестах лучше всего себя показал шаблон word1* word2*
А для доступа к значения релевантности можно прибегнуть к небольшой хитрости
Обработка поисковой фразы и выборка с регистрацией временного поля релевантности
$value = 'Олег Анатол';
// $value = Helper::matchAgainstWildcard($value);
$value = implode('* ', explode(' ', $value . '*'));
$query = PeopleTable::build()::query()
->addSelect('ID')
->addSelect('NAME')
->addSelect('MATCH_SORT')
->registerRuntimeField(new ExpressionField(
'MATCH_SORT',
"MATCH (%s) AGAINST ('$value' IN BOOLEAN MODE)",
['NAME']
))
->addOrder('MATCH_SORT', 'DESC')
->whereMatch('NAME', $value)
->fetchCollection();
Сформированный запрос
SELECT `iblock_elements_element_people`.`ID` AS `ID`, `iblock_elements_element_people`.`NAME` AS `NAME`, MATCH (`iblock_elements_element_people`.`NAME`) AGAINST ('Олег* Анатол*' IN BOOLEAN MODE) AS `MATCH_SORT` FROM `b_iblock_element` `iblock_elements_element_people` WHERE MATCH (`iblock_elements_element_people`.`NAME`) AGAINST ('Олег* Анатол*' IN BOOLEAN MODE) AND `iblock_elements_element_people`.`IBLOCK_ID` = 33 ORDER BY `MATCH_SORT` DESC
Результат выборки
Голубев Олег Анатольевич | 26.529659271240234
Олегович Анатольевичный | 26.529659271240234
Олег Анатол | 26.529659271240234
Шварценеггер Олег Петрович | 12.588944435119629
Викторович Олег | 12.588944435119629
Проблемы разделяются на две категории:
Проверки производились на версии ядра: 20.0.1198
OneToMany полем;
whereIn с пустым массивом;
UserTable;
Построение объектных выборок с агрегациями.
Ошибка говорит о том, что результат не может быть применен для объекта.
Решение: либо не используем агрегацию, либо используем
fetchObject fetch (fetchCollection fetchAll).
Лимитированная выборка с OneToMany полем.
Корректен ли будет результат выборки в следующем запросе?
Лимитированная выборка с OneToMany полем.
Происходит из-за того, что на уровне запроса, в результате получается плоский повторяющиеся список (декартово произведение множеств), по которому уже делается limit.
Лимитированная выборка с OneToMany полем.
Решение: либо делаем два отдельных запроса (первый с лимитом, второй со всеми данными от результата первого), либо используем
fill (правильный и красивый вариант).
Если в whereIn передать пустой массив.
Если в whereIn передать пустой массив.
Решение: оборачиваем в условие весь запрос.
Цепочка вызова методов объекта.
Какая есть опасность в следующей реализации?
Цепочка вызова методов объекта.
На одной из цепочки (кроме последней) может оказаться null.
Решение: каждый этап цепочки нужно проверять на null, чтобы перейти на следующую цепочку.
?-> )
Сущность UserTable.
Что будет если, попробовать внести изменение или создать пользователя?
Сущность UserTable.
UserTable является сложной сущностью, создание и изменение данных - не простой процесс.
Решение: используем CUser для операций не связанных с выборками.
Аннотации.
Аннотирование тяжелых по свойствам инфоблоков приводит к тому, что phpstorm либо долго индексирует аннотации, либо отказывается вообще их индексировать (пример: Корунд, инфоблок товаров - примерно 700 полей)
TextField);
fill для всех полей.
Создание элемента.
В версии инфоблока 2.0 нет возможности создать элемент с заполненными единичными свойствами.
Создание элемента.
Решение: либо используем старое API инфоблоков CIblockElement, либо переводим инфоблок в первую версию.
Создание элемента.
Решение: используем save на стандартные данные инфоблока, затем, save на значение свойств.
Создание элемента.
Как правильно установить тип на PREVIEW_TEXT и DETAIL_TEXT ?
Решение: устанавливаем значение в первом save, затем, тип во втором save.
Свойство типа TEXT/HTML (TextField).
Что нужно знать:
Свойство типа TEXT/HTML (TextField).
Неправильное сохранение данных.
Свойство типа TEXT/HTML (TextField).
Неправильное сохранение данных.
Проблема будет следующая: мы не указали тип данных HTML или TEXT.
Свойство типа TEXT/HTML (TextField).
Как правильно сохранить данные и установить тип (HTML или TEXT).
Решение: Нужно перед установкой сериализовать данные функцией serialize.
Свойство типа TEXT/HTML (TextField).
Особенности выборки значения.
Решение: т.к. значение данного поля является составное, то его значение хранится в сериализованном виде. При выборке, результат значения поля нужно десериализовать
функцией unserialize.
Использование fill для всех полей.
Ошибка появляется если, вы работаете с инфоблоком 2.0.
Использование fill для всех полей.
Ошибка появляется если, вы работаете с инфоблоком 2.0.
Решение: либо перевести на первую версию инфоблок, либо перечислить все поля, где для каждого нужно будет дописать .VALUE.
На самом деле, все проблемы и особенности еще не все раскрыты, т.к. еще не все известны.
По багам вопросам можно посмотреть здесь: https://github.com/medveddev/bxorm/issuesTextField из массива с сериализациями на объекты;
whereInEmpty имеющий автоматическую проверку на пустой массив;
Некоторое время назад ORM была без объектов, без данамической компиляции и многого другого. С тех пор остались описанные сущности, вызовы уже устаревших методов, описание полей массивами и т.д. и т.п.
По возможности следите за кодом с которым приходится работать. Если в нем есть куски старой ORM — исправляйте хотя бы небольшими частями