Польза
Финт с множественным свойством в D7-Orm-выборке

Финт с множественным свойством в D7-Orm-выборке

В работе я довольно часто использую хелперы, которые хранят в кеше какие-то заготовленные наборы данных. Эти данные в большинстве случаев чистятся и тут же перегенериваются через реакцию на событие (! нужно не злоупотреблять этим а анализировать — иногда лучше просто чистить кеш, без пересоздания, а формировать его при фактической выборке).

Например, у нас есть регионы (Москва, Питер, Екатеринбург, ...) — у регионов есть контакты, есть привязки к типам цен, офисы и много чего сопутствующего. Хелпер следит за кешем нужной мне структуры, которая используется в самых разных случаях (от шапки с телефонами и ленивой подгрузке через аякс... до страницы с контактами и скриптами с автоматизацией) и служит единым источником данных.

Получение значения из s-таблицы напрямую

Ситуация: вводится понятие сотрудников региона и нам нужно вклиниться в D7-ORM-выборку и получить список id.

Заводим в регионах множественное свойство элемента CITY_EMPLOYEES — привязка к элементам инфоблока.

Так как свойство множественное, каждый fetch будет формировать массив только с одним значением из всего списка множественного значения.

Нам придётся:

Так как нам достаточно просто получить список, без более глубоких джойнов, мы можем дообучить сущность специальным полем, который делал бы выборку не из m-таблицы (где все множественные значения содержатся в разделённом виде), а из s-таблицы (где массив сериализован и всё хранится вместе).

Битрикс. ORM. Хранение множественного свойства в m- и s-таблицах

Получив сериализованное значение, мы его рассериализуем и в ключе VALUE будут нужные нам ID.
...и не придётся пересобирать цикл с fetch-ами.

Расширение сущности нужным нам полем.

Компиляция сущности формирует нужное нам свойство, но оно заточено на m-таблицу. однако в этом свойстве есть всё, что нужно для получения данных из s-таблицы.

В коде ниже мы получаем из референсной сущности нужные данные (таблицу, код свойства), формируем фейковую мини-сущность и дообучаем основную сущность нужным полем.

protected static function getEntity()
{
    if (!self::$entity) {
        Loader::includeModule('iblock');

        // Компилируем основную сущность
        $elEntity = IblockTable::compileEntity(self::$regionsApi);

        // Получаем референсную сущность,
        // из которой добудем нужные нам данные
        /** @var \Bitrix\Iblock\ORM\ValueStorageEntity $employeeEntity */
        $employeeEntity = $elEntity->getField('CITY_EMPLOYEES')->getRefEntity();

        // Формируем название s-таблицы
        $dbTableName = str_replace(
            'prop_m',
            'prop_s',
            $employeeEntity->getDBTableName()
        );

        // Получаем код свойства, чтобы было "dependency injection"
        $propertyId = (int)str_replace(
            'IblockProperty',
            '',
            $employeeEntity->getName()
        );

        // Формируем фейковую сущность
        $fakeEntity = Entity::compileEntity(
            'FakeEmployeeEntity',
            [
                (new IntegerField('IBLOCK_ELEMENT_ID'))
                    ->configureAutocomplete(true)
                    ->configurePrimary(true),
                (new StringField('PROPERTY_' . $propertyId)),
                (new ExpressionField(
                    'RAW_VALUE',
                    '%s',
                    ['PROPERTY_' . $propertyId]))
            ],
            [
                'namespace' => 'AlexeyGfi',
                'table_name' => $dbTableName
            ]
        );

        // Дообучаем основную сущность новым полем
        $elEntity->addField((
            (new Reference(
                'EMPLOYEES_SERIALIZED',
                $fakeEntity,
                array('=this.ID' => 'ref.IBLOCK_ELEMENT_ID'))
            )->configureJoinType(\Bitrix\Main\Entity\Query\Join::TYPE_LEFT)));

        self::$entity = $elEntity;
    }

    return self::$entity;
}

Теперь мы в методе по получению выборки можем использовать новое поле.

В фейковой сущности мы специально завели дополнительное экспрешн-свойство RAW_VALUE, чтобы метод снаружи не пытался работать с id свойства а использовал символьный код.

$elEntity = self::getEntity();
$qRes = (new Query($elEntity))
    //...

    ->setSelect([
        //...

        // Получаем сериализованное значение
        'EMPLOYEES_SERIALIZED_VALUE' => 'EMPLOYEES_SERIALIZED.RAW_VALUE'
    ])
    ->exec();

while ($qArr = $qRes->fetch()) {

    //...

    // Рассериализовываем
    $employeeReadyData = @unserialize(
        $qArr['EMPLOYEES_SERIALIZED_VALUE'],
        ['allowed_classes' => false]
    );

    // Получаем VALUE, если оно определено
    if (is_array($employeeReadyData)) {
        $employeesIdList = $employeeReadyData['VALUE'] ?? null;
        if ($employeesIdList) {
            $res['EMPLOYEES_ID_LIST'] = $employeesIdList;
        }
    }

    // ...

}

Особенность заполнения s-таблиц

Есть одно неудобство, которое придётся учесть. При обновлении элемента, значения меняются в m-таблице, а соответствующие колонки s-таблиц чистятся.

s-таблицы заполняются при \CIBlockElement::GetList-е с fetch-ем по каждой записи.

Дописываем выборку в реакции на событие.

В коде ниже: метод, который реагирует на событие iblock / OnAfterIBlockElementUpdate. В методе идёт проверка на целевой инфоблок, выборка для актуализации данных в s-таблицах, getList для формирования нового кеша.

public static function onChangesElementEvent($arFields): void
{
    $iblockId = (int)($arFields['IBLOCK_ID'] ?? null);
    if ($iblockId !== self::$regionsIblockId) {
        return;
    }

    self::clearCache();

    //wakeUp -s- table property
    $res = \CIBlockElement::GetList(
        false,
        ['IBLOCK_ID' => self::$regionsIblockId],
        false, false,
        [
            'ID',
            'PROPERTY_CITY_EMPLOYEES'
        ]
    );

    while ($arr = $res->fetch()) {}

    // Актуализация кеша
    self::getList();
}

Обсуждение статьи

Написать