Связи и справочники на Highload-блоках

Highload-блок может хранить ссылки на записи другого Highload-блока или предоставлять значения для свойства информационного блока типа Справочник. Эти механизмы используют разные идентификаторы:

  • пользовательское поле типа hlblock хранит системный ID связанной записи,

  • свойство Справочник хранит значение поля UF_XML_ID этой записи.

Для связи записей настройте пользовательское поле с помощью класса классического API CUserTypeEntity. Для свойства информационного блока используйте тип directory и укажите таблицу Highload-блока в настройке TABLE_NAME.

Перед настройкой подготовьте Highload-блоки и поля, по которым пользователь сможет различать записи. Создание блоков и пользовательских полей описано в статье Создание, настройка и перенос Highload-блоков, а получение динамического класса данных — в статье Работа с записями.

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

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

Выбрать механизм связи

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

Задача

Механизм

Сохраненное значение

Получение связанных данных

Связать запись Highload-блока с записью другого или того же блока

Пользовательское поле типа hlblock

Числовой ID целевой записи

Поле _REF для одиночной связи или отдельная выборка для множественной

Использовать Highload-блок как источник значений свойства информационного блока

Свойство типа directory

Строковый UF_XML_ID записи справочника

Выборка записи справочника по UF_XML_ID

Не подменяйте один механизм другим. Поле hlblock предназначено для подсистемы пользовательских полей, а тип directory регистрируется как пользовательский тип свойства информационного блока.

Связать записи Highload-блоков

Пользовательское поле типа hlblock можно добавить в Highload-блок или другой объект с поддержкой пользовательских полей. Целевым может быть другой или тот же Highload-блок. Настройки поля определяют целевой блок, поле для отображения и вид элемента управления.

Настроить поле привязки

Основные настройки типа hlblock.

* — обязательная настройка помечена звездочкой.

Настройка

Тип

Допустимые значения и значение по умолчанию

Назначение

HLBLOCK_ID *

int

Положительный идентификатор. По умолчанию — 0

Определяет целевой Highload-блок

HLFIELD_ID

int

Идентификатор пользовательского поля или 0. По умолчанию — 0

Выбирает поле для отображения. Значение 0 использует системное поле ID

DISPLAY

string

LIST, CHECKBOX, UI или DIALOG. По умолчанию — LIST

Определяет вид стандартного элемента управления

LIST_HEIGHT

int

Положительное число. По умолчанию — 1 для одиночного и 5 для множественного поля

Задает высоту списка в строках

DEFAULT_VALUE

int, string или int[]

Числовой идентификатор для одиночного поля или массив идентификаторов для множественного. Одиночное значение приводится к int. По умолчанию — пустая строка или пустой массив

Задает значение поля по умолчанию

Настройка 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

Стабильное строковое значение, которое сохраняется в свойстве информационного блока

UF_NAME

Название варианта. Если поля нет, интерфейс использует UF_XML_ID

UF_SORT

Порядок вариантов. Если поля нет, записи сортируются по названию или UF_XML_ID, затем по ID

UF_FILE

Идентификатор изображения, которое может использовать интерфейс свойства

UF_DEF

Признак варианта по умолчанию. При нескольких отмеченных записях выбирается первая с учетом сортировки

UF_DESCRIPTION

Краткое описание варианта для интерфейсов, которые его поддерживают

Задайте уникальные непустые значения 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-блок справочника.

Если переименовываете таблицу с помощью кода:

  1. Получите полный массив USER_TYPE_SETTINGS и замените в нем TABLE_NAME.

  2. Создайте объект CIBlockProperty и вызовите метод Update().

  3. Проверьте возвращенное логическое значение. Текст ошибки доступен в свойстве LAST_ERROR объекта.

  4. Прочитайте свойство элемента и убедитесь, что каждому сохраненному UF_XML_ID соответствует запись блока.

Сохранить целостность связей

Ссылки Highload-блоков и значения справочника требуют правил на уровне кода проекта. Определите эти правила до импорта, удаления и изменения идентификаторов.

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

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

Проверять запись до сохранения

Для поля hlblock запросите целевую запись по ID. Для свойства directory запросите запись по точному UF_XML_ID. При множественном значении сравните полный набор переданных и найденных идентификаторов.

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

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

Обработать удаление целевой записи

Для каждой связи выберите один вариант поведения при удалении:

  • запретить удаление, пока на запись ссылаются другие объекты,

  • очистить ссылки перед удалением,

  • заменить ссылку на заранее определенную запись,

  • сохранить ссылку и обрабатывать отсутствие целевой записи как допустимое состояние.

Проверка перед удалением и само удаление не образуют атомарную операцию автоматически. Если параллельное изменение может нарушить правило, обеспечьте последовательное выполнение операции средствами проекта и повторно проверьте ссылки непосредственно перед удалением.

Переносить связи между окружениями

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

Свойство directory уже хранит UF_XML_ID. Сохраняйте этот внешний идентификатор неизменным и обеспечьте его уникальность во всех окружениях. После импорта проверьте, что каждому значению свойства соответствует ровно одна запись справочника.

Если переносите данные через административный импорт Highload-блоков, настройка «Сохранять связи со сторонними объектами» влияет на значения полей-привязок. Порядок импорта и проверка результата описаны в статье Создание, настройка и перенос Highload-блоков.

Проверить результат

Проверьте каждый механизм на тестовых данных.

  1. Создайте целевую запись и сохраните ее идентификаторы ID и UF_XML_ID.

  2. Для одиночного поля hlblock сохраните ID, получите связанную запись через _REF и сравните результат с целевой записью.

  3. Для множественного поля сохраните несколько ID, повторно прочитайте массив и проверьте отсутствие пропущенных записей пакетным запросом.

  4. Попробуйте передать отсутствующий ID и убедитесь, что код проекта отклоняет значение до сохранения.

  5. Для свойства directory сохраните UF_XML_ID, прочитайте значение свойства и найдите по нему запись справочника.

  6. Измените UF_NAME записи справочника и проверьте, что по сохраненному UF_XML_ID по-прежнему находится та же запись.

  7. Переименуйте таблицу справочника в тестовом сценарии, обновите TABLE_NAME свойства и проверьте сохраненные значения.

  8. Проверьте выбранное правило удаления: запрет, очистку, замену или допустимую ссылку на отсутствующую запись.

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

Поле типа hlblock связывает записи по системному ID, а свойство типа directory — по внешнему UF_XML_ID. Проверяйте целевую запись до сохранения, загружайте множественные связи одним запросом и задавайте правила для удаления и переноса данных.