Связи и справочники на Highload-блоках
Highload-блок может хранить ссылки на записи другого Highload-блока или предоставлять значения для свойства информационного блока типа Справочник. Эти механизмы используют разные идентификаторы:
-
пользовательское поле типа
hlblockхранит системныйIDсвязанной записи, -
свойство Справочник хранит значение поля
UF_XML_IDэтой записи.
Для связи записей настройте пользовательское поле с помощью класса классического API CUserTypeEntity. Для свойства информационного блока используйте тип directory и укажите таблицу Highload-блока в настройке TABLE_NAME.
Перед настройкой подготовьте Highload-блоки и поля, по которым пользователь сможет различать записи. Создание блоков и пользовательских полей описано в статье Создание, настройка и перенос Highload-блоков, а получение динамического класса данных — в статье Работа с записями.
Базовые поля исходного и целевого блоков можно выбрать из раздела Схема данных в примерах. Поля связей и справочника настраиваются ниже.
Оба механизма хранят значение ссылки, но не создают ограничение внешнего ключа. База данных не проверяет, существует ли целевая запись, и не очищает сохраненную ссылку после ее удаления. Проверяйте существование связанных данных в коде и определите правила удаления до запуска сценария.
Выбрать механизм связи
Выбор зависит от объекта, в котором хранится ссылка, и от того, какой идентификатор должен оставаться неизменным при переносе данных между окружениями.
|
Задача |
Механизм |
Сохраненное значение |
Получение связанных данных |
|
Связать запись Highload-блока с записью другого или того же блока |
Пользовательское поле типа |
Числовой |
Поле |
|
Использовать Highload-блок как источник значений свойства информационного блока |
Свойство типа |
Строковый |
Выборка записи справочника по |
Не подменяйте один механизм другим. Поле hlblock предназначено для подсистемы пользовательских полей, а тип directory регистрируется как пользовательский тип свойства информационного блока.
Связать записи Highload-блоков
Пользовательское поле типа hlblock можно добавить в Highload-блок или другой объект с поддержкой пользовательских полей. Целевым может быть другой или тот же Highload-блок. Настройки поля определяют целевой блок, поле для отображения и вид элемента управления.
Настроить поле привязки
Основные настройки типа hlblock.
* — обязательная настройка помечена звездочкой.
|
Настройка |
Тип |
Допустимые значения и значение по умолчанию |
Назначение |
|
|
|
Положительный идентификатор. По умолчанию — |
Определяет целевой Highload-блок |
|
|
|
Идентификатор пользовательского поля или |
Выбирает поле для отображения. Значение |
|
|
|
|
Определяет вид стандартного элемента управления |
|
|
|
Положительное число. По умолчанию — |
Задает высоту списка в строках |
|
|
|
Числовой идентификатор для одиночного поля или массив идентификаторов для множественного. Одиночное значение приводится к |
Задает значение поля по умолчанию |
Настройка MULTIPLE находится в описании пользовательского поля, а не внутри SETTINGS. Значение N создает одиночную связь, значение Y — множественную.
Добавьте в исходный Highload-блок одиночное поле UF_CATEGORY, которое ссылается на запись блока категорий. До запуска передайте идентификаторы блоков в $sourceHighloadBlockId и $targetHighloadBlockId. Переменная $targetNameFieldId должна содержать идентификатор поля UF_NAME целевого блока.
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;
if (!Loader::includeModule('highloadblock'))
{
throw new \RuntimeException('Не удалось подключить модуль highloadblock');
}
// Проверяем идентификаторы исходного и целевого блоков
$sourceHighloadBlockId = (int)$sourceHighloadBlockId;
$targetHighloadBlockId = (int)$targetHighloadBlockId;
$targetNameFieldId = (int)$targetNameFieldId;
if ($sourceHighloadBlockId <= 0 || $targetHighloadBlockId <= 0)
{
throw new \RuntimeException('Некорректный идентификатор Highload-блока');
}
$entityId = HighloadBlockTable::compileEntityId($sourceHighloadBlockId);
$userTypeEntity = new \CUserTypeEntity();
// Ищем поле, чтобы повторный запуск не создавал дубликат
$existingField = \CUserTypeEntity::GetList([], [
'ENTITY_ID' => $entityId,
'FIELD_NAME' => 'UF_CATEGORY',
])->Fetch();
if ($existingField)
{
// Проверяем настройки найденного поля
$isCompatible = (
$existingField['USER_TYPE_ID'] === 'hlblock'
&& $existingField['MULTIPLE'] === 'N'
&& (int)$existingField['SETTINGS']['HLBLOCK_ID']
=== $targetHighloadBlockId
&& (int)$existingField['SETTINGS']['HLFIELD_ID']
=== $targetNameFieldId
);
if (!$isCompatible)
{
throw new \RuntimeException(
'Поле UF_CATEGORY уже существует с другими настройками'
);
}
$categoryFieldId = (int)$existingField['ID'];
}
else
{
// Создаем поле, если оно еще не существует
$categoryFieldId = $userTypeEntity->Add([
'ENTITY_ID' => $entityId,
'FIELD_NAME' => 'UF_CATEGORY',
'USER_TYPE_ID' => 'hlblock',
'SORT' => 200,
'MULTIPLE' => 'N',
'MANDATORY' => 'N',
'SETTINGS' => [
'HLBLOCK_ID' => $targetHighloadBlockId,
'HLFIELD_ID' => $targetNameFieldId,
'DISPLAY' => 'LIST',
'LIST_HEIGHT' => 1,
'DEFAULT_VALUE' => '',
],
'EDIT_FORM_LABEL' => [
'ru' => 'Категория',
],
'LIST_COLUMN_LABEL' => [
'ru' => 'Категория',
],
'LIST_FILTER_LABEL' => [
'ru' => 'Категория',
],
]);
if (!$categoryFieldId)
{
global $APPLICATION;
$exception = $APPLICATION->GetException();
$message = $exception
? $exception->GetString()
: 'Не удалось создать поле привязки';
throw new \RuntimeException($message);
}
}
После добавления поля продолжите работу с обновленной ORM-картой в новом запросе. Если сценарий должен использовать поле сразу, примените порядок из раздела Обновить ORM-объект после изменения структуры.
Не используйте окончание _REF в коде пользовательского поля. Модуль резервирует его для ORM-ссылки, которую автоматически создает для типа hlblock.
Сохранить одиночную связь
Передавайте в поле системный ID существующей записи целевого блока. Тип hlblock использует целочисленное хранилище, но не проверяет наличие целевой записи. Выполните эту проверку отдельно.
Примеры работы со связями используют $sourceDataClass и $targetDataClass. Получите эти имена через HighloadBlockTable::compileEntity() по идентификаторам исходного и целевого блоков. Полный порядок подготовки динамического класса приведен в статье Работа с записями.
Проверьте категорию и сохраните ее идентификатор в записи исходного блока.
$categoryId = (int)$categoryId;
if ($categoryId <= 0)
{
throw new \RuntimeException('Некорректный идентификатор категории');
}
$category = $targetDataClass::getById($categoryId)->fetch();
if (!$category)
{
throw new \RuntimeException('Категория не найдена');
}
$updateResult = $sourceDataClass::update($recordId, [
'UF_CATEGORY' => $categoryId,
]);
if (!$updateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $updateResult->getErrorMessages())
);
}
Чтобы очистить необязательную одиночную связь, передайте null. После изменения повторно прочитайте запись и проверьте фактическое значение.
Получить связанную запись через ORM
Для одиночного поля модуль добавляет в ORM-карту ссылку с именем <КОД_ПОЛЯ>_REF. Для поля UF_CATEGORY ссылка называется UF_CATEGORY_REF и соединяет сохраненный идентификатор с полем ID целевого блока.
Получите исходную запись вместе с названием категории:
$record = $sourceDataClass::getRow([
'select' => [
'ID',
'UF_NAME',
'UF_CATEGORY',
'CATEGORY_NAME' => 'UF_CATEGORY_REF.UF_NAME',
],
'filter' => [
'=ID' => $recordId,
],
]);
if ($record === null)
{
throw new \RuntimeException('Исходная запись не найдена');
}
if ($record['UF_CATEGORY'] !== null && $record['CATEGORY_NAME'] === null)
{
throw new \RuntimeException('Связанная категория не найдена');
}
Через путь UF_CATEGORY_REF.<поле> можно выбирать поля целевой записи и фильтровать исходные записи. Добавляйте в select только нужные значения: соединение не требует получать всю запись целевого блока.
Получите исходные записи из активной категории с заданным кодом:
$records = $sourceDataClass::getList([
'select' => [
'ID',
'UF_NAME',
'CATEGORY_NAME' => 'UF_CATEGORY_REF.UF_NAME',
],
'filter' => [
'=UF_CATEGORY_REF.UF_ACTIVE' => 1,
'=UF_CATEGORY_REF.UF_CODE' => $categoryCode,
],
'order' => ['ID' => 'ASC'],
])->fetchAll();
Сохранить множественную связь
Множественное поле хранит массив идентификаторов. При обновлении переданный массив заменяет весь набор значений. Передайте пустой массив, чтобы удалить все связи.
Проверьте одним запросом, что все категории существуют, и сохраните их идентификаторы:
$categoryIds = array_values(array_unique(array_map(
'intval',
$categoryIds
)));
$categories = $targetDataClass::getList([
'select' => ['ID'],
'filter' => ['@ID' => $categoryIds],
])
->fetchAll()
;
$foundCategoryIds = array_map(
static fn(array $category): int => (int)$category['ID'],
$categories
);
sort($categoryIds);
sort($foundCategoryIds);
if ($categoryIds !== $foundCategoryIds)
{
throw new \RuntimeException('Одна или несколько категорий не найдены');
}
$updateResult = $sourceDataClass::update($recordId, [
'UF_CATEGORIES' => $categoryIds,
]);
if (!$updateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $updateResult->getErrorMessages())
);
}
Для множественного поля динамический ORM-объект предоставляет массив UF_CATEGORIES и служебное представление UF_CATEGORIES_SINGLE для отдельных значений. Прямой алиас UF_CATEGORIES_REF на основной объект не создается. Если нужно получить связанные записи целиком, сначала соберите идентификаторы из исходных записей, затем выполните один запрос к целевому блоку.
Получите названия категорий без отдельного запроса для каждого идентификатора:
$record = $sourceDataClass::getById($recordId)->fetch();
if (!$record)
{
throw new \RuntimeException('Исходная запись не найдена');
}
$categoryIds = array_map('intval', (array)$record['UF_CATEGORIES']);
$categoriesById = [];
if ($categoryIds !== [])
{
$categories = $targetDataClass::getList([
'select' => ['ID', 'UF_NAME'],
'filter' => ['@ID' => $categoryIds],
])->fetchAll();
foreach ($categories as $category)
{
$categoriesById[(int)$category['ID']] = $category;
}
}
$missingCategoryIds = array_values(array_diff(
$categoryIds,
array_keys($categoriesById)
));
Массив $missingCategoryIds содержит идентификаторы отсутствующих записей. Такой пакетный запрос подходит и для проверки большого набора исходных записей: сначала объедините их идентификаторы, удалите дубли, затем один раз запросите целевой блок.
Создать связь с тем же блоком
Для дерева, цепочки замен или других связей внутри одного блока укажите в HLBLOCK_ID идентификатор исходного блока. Например, поле UF_PARENT может хранить ID родительской записи.
Пользовательский тип не проверяет циклы. Перед сохранением родителя исключите как минимум ссылку записи на себя. Если структура должна быть деревом, пройдите цепочку родителей и отклоните изменение, которое замыкает цепочку на исходной записи. Определите максимальную глубину обхода, чтобы поврежденные данные не вызвали бесконечный цикл.
Использовать Highload-блок как справочник информационного блока
Свойство информационного блока типа Справочник получает варианты из Highload-блока. Настройка свойства хранит имя таблицы блока, а значение свойства элемента — внешний идентификатор UF_XML_ID, а не системный ID записи.
Подготовить поля справочника
Поле UF_XML_ID обязательно для работы типа directory. Остальные служебные поля расширяют отображение и сортировку.
|
Поле |
Назначение |
|
|
Стабильное строковое значение, которое сохраняется в свойстве информационного блока |
|
|
Название варианта. Если поля нет, интерфейс использует |
|
|
Порядок вариантов. Если поля нет, записи сортируются по названию или |
|
|
Идентификатор изображения, которое может использовать интерфейс свойства |
|
|
Признак варианта по умолчанию. При нескольких отмеченных записях выбирается первая с учетом сортировки |
|
|
Краткое описание варианта для интерфейсов, которые его поддерживают |
Задайте уникальные непустые значения UF_XML_ID и не меняйте их после использования в свойствах. Модуль ищет запись по точному совпадению внешнего идентификатора, но не создает для него уникальное ограничение автоматически.
Создать свойство типа Справочник
Подключите модули highloadblock и iblock. В USER_TYPE_SETTINGS.TABLE_NAME передайте значение TABLE_NAME подготовленного Highload-блока.
Добавьте одиночное свойство COLOR в существующий информационный блок. Переменная $directoryHighloadBlockId содержит идентификатор блока справочника, а $iblockId — идентификатор информационного блока.
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;
if (
!Loader::includeModule('highloadblock')
|| !Loader::includeModule('iblock')
)
{
throw new \RuntimeException('Не удалось подключить необходимые модули');
}
// Получаем описание Highload-блока справочника
$directory = HighloadBlockTable::getById(
$directoryHighloadBlockId
)->fetch();
if (!$directory)
{
throw new \RuntimeException('Highload-блок справочника не найден');
}
// Ищем свойство, чтобы повторный запуск не создавал дубликат
$existingProperty = \CIBlockProperty::GetList([], [
'IBLOCK_ID' => $iblockId,
'CODE' => 'COLOR',
])->Fetch();
if ($existingProperty)
{
// Проверяем тип и настройки найденного свойства
$settings = $existingProperty['USER_TYPE_SETTINGS'] ?? [];
$isCompatible = (
$existingProperty['PROPERTY_TYPE'] === 'S'
&& $existingProperty['USER_TYPE'] === 'directory'
&& $existingProperty['MULTIPLE'] === 'N'
&& ($settings['TABLE_NAME'] ?? '')
=== $directory['TABLE_NAME']
);
if (!$isCompatible)
{
throw new \RuntimeException(
'Свойство COLOR уже существует с другими настройками'
);
}
$propertyId = (int)$existingProperty['ID'];
}
else
{
// Создаем свойство, если оно еще не существует
$property = new \CIBlockProperty();
$propertyId = $property->Add([
'IBLOCK_ID' => $iblockId,
'NAME' => 'Цвет',
'CODE' => 'COLOR',
'PROPERTY_TYPE' => 'S',
'USER_TYPE' => 'directory',
'MULTIPLE' => 'N',
'SORT' => 200,
'ACTIVE' => 'Y',
'USER_TYPE_SETTINGS' => [
'TABLE_NAME' => $directory['TABLE_NAME'],
],
]);
if (!$propertyId)
{
throw new \RuntimeException($property->LAST_ERROR);
}
}
Для привязки к существующему справочнику достаточно передать строковую настройку TABLE_NAME. Если остальные настройки отсутствуют, тип свойства использует высоту списка 1, автоматическую ширину и значения N для group и multiple.
Параметр свойства MULTIPLE определяет, сколько значений можно сохранить:
-
N— создает одиночное свойство, -
Y— множественное.
Значение по умолчанию — N. Не используйте настройку USER_TYPE_SETTINGS.multiple вместо параметра свойства.
Сохранить значение справочника
Передавайте в свойство значение UF_XML_ID выбранной записи. Системный ID Highload-блока для этого сценария не подходит.
Проверьте внешний идентификатор и сохраните его в свойстве COLOR элемента информационного блока:
$directoryEntity = HighloadBlockTable::compileEntity($directory);
$directoryDataClass = $directoryEntity->getDataClass();
$directoryItem = $directoryDataClass::getRow([
'select' => ['ID', 'UF_XML_ID'],
'filter' => ['=UF_XML_ID' => $colorXmlId],
]);
if ($directoryItem === null)
{
throw new \RuntimeException('Значение справочника не найдено');
}
\CIBlockElement::SetPropertyValuesEx($elementId, $iblockId, [
'COLOR' => $colorXmlId,
]);
Метод SetPropertyValuesEx() не возвращает объект результата. До вызова убедитесь, что код выполняется с правами на изменение элемента информационного блока. После вызова прочитайте свойство и сравните сохраненное значение с $colorXmlId.
Для множественного свойства передайте массив внешних идентификаторов. Предварительно получите все записи одним запросом с фильтром @UF_XML_ID и сравните найденные значения с исходным набором. После сохранения повторно прочитайте все значения свойства. Не используйте их порядок как часть логики.
Получить данные справочника
Значение свойства информационного блока содержит UF_XML_ID. Сначала прочитайте свойство элемента, затем запросите динамический класс справочника по полученным значениям.
$propertyResult = \CIBlockElement::GetProperty(
$iblockId,
$elementId,
'sort',
'asc',
['CODE' => 'COLOR']
);
$propertyValues = [];
while ($property = $propertyResult->Fetch())
{
if ((string)$property['VALUE'] !== '')
{
$propertyValues[] = (string)$property['VALUE'];
}
}
$propertyValues = array_values(array_unique($propertyValues));
$directoryItemsByXmlId = [];
if ($propertyValues !== [])
{
$directoryItems = $directoryDataClass::getList([
'select' => [
'ID',
'UF_XML_ID',
'UF_NAME',
],
'filter' => [
'@UF_XML_ID' => $propertyValues,
],
])->fetchAll();
foreach ($directoryItems as $directoryItem)
{
$directoryItemsByXmlId[$directoryItem['UF_XML_ID']]
= $directoryItem;
}
}
$missingXmlIds = array_values(array_diff(
$propertyValues,
array_keys($directoryItemsByXmlId)
));
if ($missingXmlIds !== [])
{
throw new \RuntimeException(
'Часть значений свойства не найдена в справочнике'
);
}
Код одинаково обрабатывает одиночное и множественное свойство. Для списка элементов соберите значения всех элементов, удалите дубли и запросите справочник один раз. Сохраните соответствие элемента и его значений отдельно, а записи справочника возьмите из словаря $directoryItemsByXmlId.
Обновить настройку после переименования таблицы
Свойство типа directory хранит значение TABLE_NAME в настройках. После переименования таблицы Highload-блока прежнее имя больше не соответствует блоку, поэтому варианты свойства перестают загружаться.
Сразу после управляемого переименования откройте настройки свойства и заново выберите Highload-блок справочника.
Если переименовываете таблицу с помощью кода:
-
Получите полный массив
USER_TYPE_SETTINGSи замените в немTABLE_NAME. -
Создайте объект
CIBlockPropertyи вызовите методUpdate(). -
Проверьте возвращенное логическое значение. Текст ошибки доступен в свойстве
LAST_ERRORобъекта. -
Прочитайте свойство элемента и убедитесь, что каждому сохраненному
UF_XML_IDсоответствует запись блока.
Сохранить целостность связей
Ссылки Highload-блоков и значения справочника требуют правил на уровне кода проекта. Определите эти правила до импорта, удаления и изменения идентификаторов.
Проверка целевой записи показана выше в примерах сохранения одиночной, множественной связи и значения справочника. Универсального кода для удаления и переноса нет: реализация зависит от правил и способа хранения данных в проекте.
Если правило нужно применять при каждой операции, зарегистрируйте обработчик динамического ORM-класса. Выбор события и отмена удаления описаны в статье События записей и права доступа.
Проверять запись до сохранения
Для поля hlblock запросите целевую запись по ID. Для свойства directory запросите запись по точному UF_XML_ID. При множественном значении сравните полный набор переданных и найденных идентификаторов.
Проверяйте права отдельно для исходного и целевого блока. Прямые вызовы динамического класса не применяют права текущего пользователя автоматически. Перед SetPropertyValuesEx() также убедитесь по правилам проекта, что код выполняется с правами на изменение элемента информационного блока.
Проверяйте целевую запись и сохраняйте ссылку в отдельных операциях. Целевая запись может исчезнуть между ними. Если такая гонка недопустима, обеспечьте последовательное выполнение обоих действий средствами проекта и повторно проверьте связь после сохранения.
Обработать удаление целевой записи
Для каждой связи выберите один вариант поведения при удалении:
-
запретить удаление, пока на запись ссылаются другие объекты,
-
очистить ссылки перед удалением,
-
заменить ссылку на заранее определенную запись,
-
сохранить ссылку и обрабатывать отсутствие целевой записи как допустимое состояние.
Проверка перед удалением и само удаление не образуют атомарную операцию автоматически. Если параллельное изменение может нарушить правило, обеспечьте последовательное выполнение операции средствами проекта и повторно проверьте ссылки непосредственно перед удалением.
Переносить связи между окружениями
Системные ID записей могут различаться между окружениями. Для переноса связей между Highload-блоками используйте стабильный внешний код в целевом блоке: найдите запись по этому коду и сохраните ее локальный ID в поле типа hlblock.
Свойство directory уже хранит UF_XML_ID. Сохраняйте этот внешний идентификатор неизменным и обеспечьте его уникальность во всех окружениях. После импорта проверьте, что каждому значению свойства соответствует ровно одна запись справочника.
Если переносите данные через административный импорт Highload-блоков, настройка «Сохранять связи со сторонними объектами» влияет на значения полей-привязок. Порядок импорта и проверка результата описаны в статье Создание, настройка и перенос Highload-блоков.
Проверить результат
Проверьте каждый механизм на тестовых данных.
-
Создайте целевую запись и сохраните ее идентификаторы
IDиUF_XML_ID. -
Для одиночного поля
hlblockсохранитеID, получите связанную запись через_REFи сравните результат с целевой записью. -
Для множественного поля сохраните несколько
ID, повторно прочитайте массив и проверьте отсутствие пропущенных записей пакетным запросом. -
Попробуйте передать отсутствующий
IDи убедитесь, что код проекта отклоняет значение до сохранения. -
Для свойства
directoryсохранитеUF_XML_ID, прочитайте значение свойства и найдите по нему запись справочника. -
Измените
UF_NAMEзаписи справочника и проверьте, что по сохраненномуUF_XML_IDпо-прежнему находится та же запись. -
Переименуйте таблицу справочника в тестовом сценарии, обновите
TABLE_NAMEсвойства и проверьте сохраненные значения. -
Проверьте выбранное правило удаления: запрет, очистку, замену или допустимую ссылку на отсутствующую запись.
-
Повторите импорт и убедитесь, что он не создает дубли для стабильных внешних кодов, а каждому коду соответствует локальная запись.
Поле типа hlblock связывает записи по системному ID, а свойство типа directory — по внешнему UF_XML_ID. Проверяйте целевую запись до сохранения, загружайте множественные связи одним запросом и задавайте правила для удаления и переноса данных.