Финт с множественным свойством в D7-Orm-выборке
В работе я довольно часто использую хелперы, которые хранят в кеше какие-то заготовленные наборы данных. Эти данные в большинстве случаев чистятся и тут же перегенериваются через реакцию на событие (! нужно не злоупотреблять этим а анализировать — иногда лучше просто чистить кеш, без пересоздания, а формировать его при фактической выборке).
Например, у нас есть регионы (Москва, Питер, Екатеринбург, ...) — у регионов есть контакты, есть привязки к типам цен, офисы и много чего сопутствующего. Хелпер следит за кешем нужной мне структуры, которая используется в самых разных случаях (от шапки с телефонами и ленивой подгрузке через аякс... до страницы с контактами и скриптами с автоматизацией) и служит единым источником данных.
Получение значения из s-таблицы напрямую
Ситуация: вводится понятие сотрудников региона и нам нужно вклиниться в D7-ORM-выборку и получить список id.
Заводим в регионах множественное свойство элемента CITY_EMPLOYEES — привязка к элементам инфоблока.
Так как свойство множественное, каждый fetch будет формировать массив только с одним значением из всего списка множественного значения.
Нам придётся:
- либо переделывать выборку на fetchObject и переписывать все значения через get-ы и коллекции;
- либо фетчить всё столько раз, сколько значений множественных свойств, опять же переписывая выборку так, чтобы свойства накапливались и в результате значения распределялись по массивам.
Так как нам достаточно просто получить список, без более глубоких джойнов, мы можем дообучить сущность специальным полем, который делал бы выборку не из 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();
}
Обсуждение статьи