Deep Bitrix ORM (Object-Relational Mapping)

Глубокое погружение в ORM. От основ до решения нетиповых проблем и новинок

О чем поговорим?

Что такое ORM

ORM — Object-Relational Mapping, что в переводе «объектно-реляционное отображение».

Суть технологии заключается в описании таблицы для доступа к ней с помощью средств ООП

Что такое Bitrix ORM

Имеет достаточно гибкий функционал для работы с сущностями, их генерацией налету, именованными методами, наличием обёрток и помощников для построения запросов

Также поставляется с большим количеством уже готовых сущностей модулей ядра

Документация Bitrix ORM

https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&CHAPTER_ID=05748

https://github.com/medveddev/bxorm

История развития

С версии ядра 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

Первый возвращает название таблицы в БД. Второй — массив полей сущности

Стандартная сущность. Highload-блоков

Наследуется от \Bitrix\Highloadblock\DataManager который расширяет \Bitrix\Main\ORM\Data\DataManager

Стоит определить метод getHighloadBlock который возвращает массив данных о highload-блоке

Стандартная сущность. Highload-блоков

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

Пример описания стандартной сущности

Поля сущности

Все поля являются потомками класса \Bitrix\Main\ORM\Fields\Field

На текущий момент их более двадцати

Скалярные поля

abstract class ScalarField extends Field implements IStorable, ITypeHintable

Поля связей

abstract class Relation extends Field implements ITypeHintable

Другие поля

Другие поля. ExpressionField

Важно знать о возможностях поля. Оно позволяет не хранить данные в таблице, а зарегистрировать виртуальное поле базирующиеся на SQL выражении

        new ExpressionField(
            'SRC_PREVIEW_URL',
            'CONCAT("/upload/", %s, \'/\', %s)',
            [
                'SRC_PREVIEW_FILE.SUBDIR',
                'SRC_PREVIEW_FILE.FILE_NAME',
            ]
        )
    

Динамическая (Ручная компиляция)

Это сущность описание которой выполняется непосредственно в момент вызова определенного метода

Есть ряд готовых методов, которые позволяют компилировать разные по специфике сущности

Динамическая (Ручная компиляция). Элементы инфоблоков

Динамическая (Ручная компиляция). Разделы инфоблоков

Динамическая (Ручная компиляция). Элементы Highload-блоков

Динамическая (Автоматическая компиляция)

Сущность компилируемая на лету без предварительного вызова каких либо методов

Обращение через имя Element{API_CODE}Table

На данный момент возможно только для сущности элементов инфоблока — таблица b_iblock_element. Для работы необходим заполненный ApiCode инфоблока

Перед началом работы. Аннотации

Физически большинства методов и таблиц нет, поэтому IDE не сможет вам построить подсказки

Для быстрой и удобной работы в IDE нужно сгенерировать аннотации

Генерация аннотации

Генерация аннотации. Composer

Установка зависимостей composer

cd bitrix && COMPOSER=composer-bx.json composer install

Генерация аннотации. Composer

Можно подключить в свой 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::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);

Query. Объект запроса

Базовый синтаксис. Query. Добавление полей в выбору

Обращение к полям связей происходит через точку

Базовый синтаксис. Query. Добавление полей в выбору

        ElementTable::query()
            ->setSelect(['NAME', 'CITY.NAME']) // Плохо
            ->addSelect('NAME')
            ->addSelect('CITY.NAME');
    

Базовый синтаксис. Query. Добавление новых полей в процессе выборки

registerRuntimeField(object: Field)

        registerRuntimeField(new Reference(
            'REF_CITY',
            CityTable::class,
            Join::on('this.CITY.VALUE', 'ref.ID')
        ))
    

Базовый синтаксис. Query. Добавление новых полей в процессе выборки

registerRuntimeField(string: name, array fieldInfo) # Плохо

        registerRuntimeField('REF_CITY', [
            'data_type' => CityTable::class,
            'reference' => ['=this.CITY.VALUE' => 'ref.ID'],
            'join_type' => 'LEFT',
        ])
    

Базовый синтаксис. Query. Добавление новых полей в процессе выборки

Данный вариант менее предпочтительный, чем добавление поля в описание сущности

Придется дублировать код описания при каждой выборке, также к нему нельзя обратиться через магический get{Name}

Базовый синтаксис. Query. Добавление новых полей в процессе выборки

        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')
            ));
    

Базовый синтаксис. Query. Фильтр запроса. Старый синтаксис

Базовый синтаксис. Query. Фильтр запроса. Старый синтаксис

        ElementTable::query()
            ->setFilter(['=ID' => 10])
            ->addFilter('=ID', 10);
    

Базовый синтаксис. Query. Фильтр запроса. Актуальный синтаксис

Помимо where есть заготовленные методы для разных операторов фильтра. Также для каждого из типов фильтра есть отрицательный вариант whereNot или where{Operator}Not

Базовый синтаксис. Query. Фильтр запроса. Актуальный синтаксис

Базовый синтаксис. Query. Фильтр запроса. Актуальный синтаксис

Мало кто знает, но 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. Фильтр запроса. Сложные правила

Все добавленные фильтры в query имеют логику AND

Если нужно сделать более сложные или вложенные правила, то можно в where передать объект фильтра

Базовый синтаксис. Query. Фильтр запроса. Сложные правила

        use \Bitrix\Iblock\ORM\Query;
        $filter = Query::filter()
            ->logic(Query::filter()::LOGIC_OR)
            ->where('CODE', 'test')
            ->where('CODE', 'test_test')
            ->whereNull('CODE');
         
        // Сделать все условия фильтра негативными
        // $filter->negative();
    

Базовый синтаксис. Query. Фильтр запроса. Сложные правила

        ElementTable::query()
            ->whereNotNull('CITY')
            ->whereBetween('ID', 1, 100)
            ->whereIn('USER.LOGIN', [
                new Column('USER.NAME'),
                new Column('USER.LAST_NAME')
            ])
            ->where($filter);
    

Базовый синтаксис. Query. Фильтр запроса. Сложные правила

Если понадобится список операторов фильтра можно посмотреть тут:

Базовый синтаксис. Query. Остальное

Нужно ли использовать exec() перед fetchCollection() или fetchObject()?

Базовый синтаксис. Query. Получение результата

Если для рабочей сущности были заранее сгенерированы аннотации, то exec можно опустить. В противном случае его обязательно нужно указывать, для корректной работы подсказок IDE

$query->fetchCollection();
$query->exec()->fetchCollection();
$query->fetchObject();
$query->exec()->fetchObject();

Базовый синтаксис. Query. Получение результата

        // 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();
        }
    

EntityObject. Объект элемента

Базовый синтаксис. EntityObject. Setters

При наличии аннотаций доступны магические аналоги конкретных полей

Базовый синтаксис. EntityObject. Getters

При наличии аннотаций доступны магические аналоги конкретных полей

Базовый синтаксис. EntityObject. Getters

Если нужно быстро получить массив значений всех полей, то можно воспользоваться методом collectValues()

Особенно удобно, когда в выборке только скалярные поля. Дабы код был типизирован и читаем следует использовать только где это правда необходимо, а не всегда!)

Базовый синтаксис. EntityObject. Множественные отношения

При наличии аннотаций доступны магические аналоги конкретных полей

Базовый синтаксис. EntityObject. Проверки полей

При наличии аннотаций доступны магические аналоги конкретных полей

Базовый синтаксис. EntityObject. Свойства объекта

Базовый синтаксис. EntityObject. Остальное

Базовый синтаксис. EntityObject. Остальное

        # Возможные маски полей 
# \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

Collection. Объект коллекции элементов

Базовый синтаксис. Collection

Класс коллекции реализует интерфейсы ArrayAccess, Iterator, Countable

Для доступа к элементам можно перебирать коллекцию в цикле или же использовать метод getAll(), который вернет содержащиеся в ней объекты

Базовый синтаксис. Collection

Базовый синтаксис. Collection

Аналогично предыдущему слайду, но в параметра передается primary объекта

Базовый синтаксис. Collection

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

Базовый синтаксис. Collection

Решение типичных задач

Решение типичных задач

Далее будут примеры работы с несколькими инфоблоками

Решение типичных задач

Задача №1

Построить отображение имен жителей по 5 с пагинацией на ORM

Решение типичных задач. Задача №1

Создаем и заполняем объект пагинации
\Bitrix\Main\UI\PageNavigation

        $nav = new PageNavigation('people-nav');
        $nav->allowAllRecords(true);
        $nav->setPageSize(5);
        $nav->initFromUri();
    

Решение типичных задач. Задача №1

Формируем выборку

        $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());
    

Решение типичных задач. Задача №1

Вывод имен

        foreach ($collection as $item) {
            echo $item->getName() . '<br>';
        }
    

Решение типичных задач. Задача №1

Вывод пагинации. Для этого есть готовый компонент в параметры которого нужно передать объект пагинации (сформированный ранее)

        $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');
    
Решение задачи №1

Решение типичных задач

Задача №2

Добавить к предыдущим условиям отображение домов жителей (множественное свойство)

Решение типичных задач. Задача №2

Если используется лимит в запросе, то множественные поля нельзя добавлять в выборку — результат будет некорректный

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

Решение — поместить все множественные поля в fill() коллекции

Решение типичных задач. Задача №2

Заполняем недостающие данные методом 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());
    

Решение типичных задач. Задача №2

Правим цикл

        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>';
        }
    
Решение задачи №2

Решение типичных задач

Задача №3

Добавить к предыдущим условиям отображение названия города

Решение типичных задач. Задача №3

Есть идеи как это сделать?

Решение типичных задач. Задача №3

Решение типичных задач. Задача №3

Самый поверхностный вариант — это делать подзапрос в другую таблицу по полученному ID дома. Нежизнеспособный вариант и вот почему:

Решение типичных задач. Задача №3

Правильный вариант — это добавление новых связей для получениях их в одном запросе

Самый правильный вариант — это модификация сущности, чтобы эти поля были доступны по всему коду проекта

Пока пойдем по первому варианту

Решение типичных задач. Задача №3

Пагинация

        $nav = new PageNavigation('people-nav');
        $nav->allowAllRecords(true)
            ->setPageSize(5)
            ->initFromUri();
    

Решение типичных задач. Задача №3

Получаем объект таблицы и сущности

        $table  = new ElementPeopleTable();
        $entity = $table::getEntity();
    

Решение типичных задач. Задача №3

Получаем нужное поле сущности, помогаем IDE, указывая её реальный класс, и добавляем необходимую связь

        /** @var PropertyOneToMany $homeProperty */
        $homeProperty = $entity->getField('HOME');
        $homeProperty->getRefEntity()->addField(new Reference(
            'ITEM',
            ElementHomeTable::getEntity(),
            Join::on('this.VALUE', 'ref.ID')
        ));
    

Решение типичных задач. Задача №3

Далее обращаемся к таблице для построения запроса

        $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());
    

Решение типичных задач. Задача №3

Правим работу с данными в цикле

        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>';
        }
    

Решение типичных задач. Задача №3

Вывод пагинации

        $APPLICATION->IncludeComponent(
            'bitrix:main.pagenavigation',
            '',
            [
                'NAV_OBJECT' => $nav,
                'SEF_MODE'   => 'N',
            ],
            false
        );
    
Решение задачи №3

Расширение таблиц

Расширение таблиц

Стандартную сущность можно и нужно расширять путем наследования, а как быть с динамическими сущностями?

Ответ: никак. При попытке наследования от такой сущности вы получите ошибку. Есть только косвенное решение

Расширение таблиц. Стандартная сущность

Как пример можно рассмотреть расширение таблицы свойств

Добавим возможность получить коллекцию значений свойства

Расширение таблиц. Стандартная сущность

Наследуем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

Создаем абстрактный класс 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() {...}
        }
    

Расширение таблиц. Динамическая сущность. TableConfigurator

Закрытое свойство $tables хранит сконфигурированные экземпляры таблиц

        private static $tables;
    

Расширение таблиц. Динамическая сущность. TableConfigurator

Закрытый статичный метод 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];
            }
        }
    

Расширение таблиц. Динамическая сущность. TableConfigurator

Публичный метод build() возвращает экземпляр таблицы

        public static function build()
        {
            return static::getInstance();
        }
    

Расширение таблиц. Динамическая сущность. TableConfigurator

Абстрактный защищенный метод configure() — это основной метод конфигурации сущности

        abstract protected static function configure(): void;
    

Расширение таблиц. Динамическая сущность. TableConfigurator

Абстрактный защищенный метод 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>';
        }
         
        // Вызов компонента пагинации
    
Решение задачи №3 после модификации сущностей

Расширение таблиц. Динамическая сущность. TableConfigurator

Плюсы

Расширение таблиц. Динамическая сущность. TableConfigurator

Минусы

Решение нетипичных задач

Решение нетипичных задач

Пример №1. Полнотекстовый поиск

Сначала необходимо с помощь миграций добавить индекс полям

Решение нетипичных задач. Полнотекстовый поиск

Метод 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, а также помощник

Решение нетипичных задач. Полнотекстовый поиск

Синтаксис оператора 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

Общие проблемы с ORM

Mapping сущности с агрегированным полем
Mapping сущности с агрегированным полем - ошибка при объектной выборке

Общие проблемы с ORM

Построение объектных выборок с агрегациями.

Ошибка говорит о том, что результат не может быть применен для объекта.

Решение: либо не используем агрегацию, либо используем fetchObject fetch (fetchCollection fetchAll).

Общие проблемы с ORM

Лимитированная выборка с OneToMany полем.

Корректен ли будет результат выборки в следующем запросе?

Лимитированный запрос с OneToMany полем
Лимитированный запрос с OneToMany полем - результат

Общие проблемы с ORM

Лимитированная выборка с OneToMany полем.

Происходит из-за того, что на уровне запроса, в результате получается плоский повторяющиеся список (декартово произведение множеств), по которому уже делается limit.

Лимитированный запрос с OneToMany полем - sql запрос до лимита
Лимитированный запрос с OneToMany полем - sql запрос после лимита

Общие проблемы с ORM

Лимитированная выборка с OneToMany полем.

Решение: либо делаем два отдельных запроса (первый с лимитом, второй со всеми данными от результата первого), либо используем fill (правильный и красивый вариант).

Лимитированный запрос с OneToMany полем - использование fill

Общие проблемы с ORM

Если в whereIn передать пустой массив.

whereIn и пустой массив - результат

Общие проблемы с ORM

Если в whereIn передать пустой массив.

Решение: оборачиваем в условие весь запрос.

whereIn и пустой массив - как сделать правильно

Общие проблемы с ORM

Цепочка вызова методов объекта.

Какая есть опасность в следующей реализации?

Цепочка вызова методов объекта

Общие проблемы с ORM

Цепочка вызова методов объекта.

На одной из цепочки (кроме последней) может оказаться null.

Решение: каждый этап цепочки нужно проверять на null, чтобы перейти на следующую цепочку.

Или переходим на php 8, и используем null-safe оператор ?-> )
Цепочка вызова методов объекта - решение

Общие проблемы с ORM

Сущность UserTable.

Что будет если, попробовать внести изменение или создать пользователя?

UserTable - обновление данных - запрос
UserTable - обновление данных - ошибка

Общие проблемы с ORM

Сущность UserTable.

UserTable является сложной сущностью, создание и изменение данных - не простой процесс.

Решение: используем CUser для операций не связанных с выборками.

Общие проблемы с ORM

Аннотации.

Аннотирование тяжелых по свойствам инфоблоков приводит к тому, что phpstorm либо долго индексирует аннотации, либо отказывается вообще их индексировать (пример: Корунд, инфоблок товаров - примерно 700 полей)

Проблемы присущие ORM для инфоблоков

Проблемы присущие ORM для инфоблоков

Создание элемента.

В версии инфоблока 2.0 нет возможности создать элемент с заполненными единичными свойствами.

Создание элемента - запрос
Создание элемента - результат

Проблемы присущие ORM для инфоблоков

Создание элемента.

Решение: либо используем старое API инфоблоков CIblockElement, либо переводим инфоблок в первую версию.

Создание элемента v1 - запрос
Создание элемента v1 - результат и еще одна проблема

Проблемы присущие ORM для инфоблоков

Создание элемента.

Решение: используем save на стандартные данные инфоблока, затем, save на значение свойств.

Создание элемента v1 - запрос c двойным save
Создание элемента v1 - запрос c двойным save - результат

Проблемы присущие ORM для инфоблоков

Создание элемента.

Как правильно установить тип на PREVIEW_TEXT и DETAIL_TEXT ?

Решение: устанавливаем значение в первом save, затем, тип во втором save.

Создание элемента v1 - запрос c двойным save - textType

Проблемы присущие ORM для инфоблоков

Свойство типа TEXT/HTML (TextField).

Что нужно знать:

Проблемы присущие ORM для инфоблоков

Свойство типа TEXT/HTML (TextField).

Неправильное сохранение данных.

TextField - неправильное сохранение данных

Проблемы присущие ORM для инфоблоков

Свойство типа TEXT/HTML (TextField).

Неправильное сохранение данных.

Проблема будет следующая: мы не указали тип данных HTML или TEXT.

TextField - неправильное сохранение данных - не указан тип

Проблемы присущие ORM для инфоблоков

Свойство типа TEXT/HTML (TextField).

Как правильно сохранить данные и установить тип (HTML или TEXT).

Решение: Нужно перед установкой сериализовать данные функцией serialize.

TextField - правильное сохранение данных

Проблемы присущие ORM для инфоблоков

Свойство типа TEXT/HTML (TextField).

Особенности выборки значения.

Решение: т.к. значение данного поля является составное, то его значение хранится в сериализованном виде. При выборке, результат значения поля нужно десериализовать функцией unserialize.

TextField - результат выборки
TextField - выборка
TextField - результат десериализации

Проблемы присущие ORM для инфоблоков

Использование fill для всех полей.

Ошибка появляется если, вы работаете с инфоблоком 2.0.

fill - для всех полей - запрос
fill - для всех полей - запрос - ошибка

Проблемы присущие ORM для инфоблоков

Использование fill для всех полей.

Ошибка появляется если, вы работаете с инфоблоком 2.0.

Решение: либо перевести на первую версию инфоблок, либо перечислить все поля, где для каждого нужно будет дописать .VALUE.

Проблемы и особенности использования

На самом деле, все проблемы и особенности еще не все раскрыты, т.к. еще не все известны.

По багам вопросам можно посмотреть здесь: https://github.com/medveddev/bxorm/issues

Пожелания

Послесловие

Некоторое время назад ORM была без объектов, без данамической компиляции и многого другого. С тех пор остались описанные сущности, вызовы уже устаревших методов, описание полей массивами и т.д. и т.п.

По возможности следите за кодом с которым приходится работать. Если в нем есть куски старой ORM — исправляйте хотя бы небольшими частями

Уходить от этого:

orm раньше

И от этого:

orm раньше запрос

Писать или переписать на это:

orm правильно

Спасибо

Вопросы?