Работа с записями
Динамический ORM-класс позволяет читать и изменять записи Highload-блока. Класс формируется для конкретного блока и предоставляет методы getList(), add(), update() и delete().
Перед работой подготовьте Highload-блок и его пользовательские поля. Управление блоком и структурой полей описано в статье Создание, настройка и перенос Highload-блоков.
Коды и типы полей, которые используются ниже, приведены в разделе Схема данных в примерах.
Общие правила запросов приведены в статье Выборка данных ORM.
Примеры статьи используют поля:
-
UF_NAME— обязательное строковое название записи, -
UF_CODE— строковый внешний код, -
UF_ACTIVE— логический признак активности, -
UF_TAGS— множественное строковое поле, -
UF_FILE— одиночное файловое поле, -
UF_FILES— множественное файловое поле.
Укажите вместо них коды полей своего Highload-блока. Перед сохранением сверяйте их с картой полей ORM-объекта. Методы add() и update() выбрасывают исключение, если переданный код отсутствует в карте или не относится к скалярным полям.
Прямые вызовы динамического ORM-класса не проверяют права текущего пользователя автоматически. Перед чтением или изменением данных проверьте в коде приложения, разрешена ли операция.
Получить класс данных
Подключите модуль highloadblock, получите описание блока и передайте его в метод HighloadBlockTable::compileEntity(). Метод getDataClass() вернет полное имя динамического класса.
Пример. Получите класс данных по идентификатору существующего Highload-блока. До запуска передайте идентификатор в переменной $highloadBlockId.
use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Loader;
if (!Loader::includeModule('highloadblock'))
{
throw new \RuntimeException('Не удалось подключить модуль highloadblock');
}
$highloadBlock = HighloadBlockTable::getById($highloadBlockId)->fetch();
if (!$highloadBlock)
{
throw new \RuntimeException('Highload-блок не найден');
}
$entity = HighloadBlockTable::compileEntity($highloadBlock);
$dataClass = $entity->getDataClass();
Переменная $dataClass содержит имя класса для дальнейших запросов. Компилировать ORM-объект перед каждой операцией в одном запросе не нужно.
Примеры ниже предполагают, что структура блока не менялась в текущем запросе. После структурного изменения выберите действие по таблице из раздела Обновить ORM-объект после изменения структуры.
Проверить права текущего пользователя
Перед каждым прямым ORM-вызовом проверьте операцию, которая соответствует действию: hl_element_read для чтения, hl_element_write для добавления и изменения или hl_element_delete для удаления. Динамический класс не выполняет эту проверку автоматически.
Получение операций текущего пользователя, обработка административного и фонового контекста описаны в разделе Проверить права доступа.
Прочитать записи
Динамический класс поддерживает короткий поиск по первичному ключу и выборку по параметрам. Формат результата зависит от способа чтения: массив, ORM-объект или коллекция.
Получить одну запись
Метод getById() создает запрос по системному идентификатору ID. Метод fetch() возвращает массив значений или false, если запись не найдена.
$record = $dataClass::getById($recordId)->fetch();
if (!$record)
{
throw new \RuntimeException('Запись не найдена');
}
Если поиск идет не по первичному ключу, используйте getRow(). Метод возвращает один массив или null.
$record = $dataClass::getRow([
'select' => ['ID', 'UF_NAME', 'UF_CODE'],
'filter' => ['=UF_CODE' => $externalCode],
]);
Фильтр с оператором = ищет точное значение. Если поле UF_CODE используется как внешний ключ интеграции, обеспечьте его уникальность на уровне проекта. Сам вызов getRow() не проверяет, существуют ли другие записи с тем же значением.
Получить список
Метод getList() принимает параметры select, filter, order, limit и offset. Указывайте только нужные поля и стабильную сортировку.
Пример. Получите до 20 активных записей, название которых содержит значение из $search.
$result = $dataClass::getList([
'select' => ['ID', 'UF_NAME', 'UF_CODE'],
'filter' => [
'=UF_ACTIVE' => 1,
'%UF_NAME' => $search,
],
'order' => [
'UF_NAME' => 'ASC',
'ID' => 'ASC',
],
'limit' => 20,
]);
$records = [];
while ($record = $result->fetch())
{
$records[] = $record;
}
Параметры запроса:
-
selectограничивает набор возвращаемых полей, -
filterзадает условия отбора, -
orderопределяет порядок записей, -
limitограничивает размер результата, -
offsetпропускает указанное количество записей.
Метод fetchAll() возвращает все строки результата одним массивом. Для большого набора обрабатывайте строки через fetch(), чтобы не собирать весь результат в памяти.
Добавить постраничную навигацию
Для постраничной выборки вычислите offset по номеру и размеру страницы. Добавьте count_total, если интерфейсу нужно общее количество записей, которые подходят под фильтр.
$page = max(1, (int)$page);
$pageSize = 20;
$result = $dataClass::getList([
'select' => ['ID', 'UF_NAME', 'UF_CODE'],
'filter' => ['=UF_ACTIVE' => 1],
'order' => ['ID' => 'ASC'],
'limit' => $pageSize,
'offset' => ($page - 1) * $pageSize,
'count_total' => true,
]);
$records = $result->fetchAll();
$totalCount = $result->getCount();
Сортировка по ID делает границы страниц однозначными для неизменного набора данных. Если параллельный процесс добавляет или удаляет записи, состав страниц с offset может измениться между запросами.
Обработать большой набор пакетами
Для фоновой обработки обходите записи по возрастающему ID, а не через растущий offset. Зафиксируйте верхнюю границу до начала цикла, чтобы записи, добавленные параллельно, не расширяли текущий запуск.
$lastRecord = $dataClass::getRow([
'select' => ['ID'],
'order' => ['ID' => 'DESC'],
]);
$maxId = (int)($lastRecord['ID'] ?? 0);
$lastId = 0;
$batchSize = 500;
while ($lastId < $maxId)
{
$records = $dataClass::getList([
'select' => ['ID', 'UF_NAME', 'UF_CODE'],
'filter' => [
'>ID' => $lastId,
'<=ID' => $maxId,
],
'order' => ['ID' => 'ASC'],
'limit' => $batchSize,
])
->fetchAll()
;
if ($records === [])
{
break;
}
foreach ($records as $record)
{
// Обработайте запись и проверьте результат изменения
$lastId = (int)$record['ID'];
}
}
Сохраняйте $lastId во внешнем состоянии задания, если обработку нужно продолжить после сбоя. Пример фиксирует верхнюю границу по ID, поэтому записи, добавленные после запуска, перейдут в следующий запуск. Такой обход не блокирует параллельные изменения существующих записей и не создает неизменный снимок уже существующих данных.
Вариант без верхней границы и критерии выбора размера пакета приведены в разделе Ограничить число записей.
Получить ORM-объект или коллекцию
Метод fetchObject() возвращает один ORM-объект или null. Метод объекта get() принимает код поля.
$recordObject = $dataClass::getById($recordId)->fetchObject();
if ($recordObject === null)
{
throw new \RuntimeException('Запись не найдена');
}
$name = $recordObject->get('UF_NAME');
Метод fetchCollection() возвращает типизированную коллекцию ORM-объектов. Коллекцию можно перебрать через foreach или преобразовать в массив методом collectValues().
$recordCollection = $dataClass::getList([
'select' => ['ID', 'UF_NAME', 'UF_CODE'],
'filter' => ['=UF_ACTIVE' => 1],
'order' => ['ID' => 'ASC'],
])
->fetchCollection()
;
foreach ($recordCollection as $recordObject)
{
$recordId = $recordObject->get('ID');
$recordName = $recordObject->get('UF_NAME');
}
Используйте массивы для простого чтения и передачи данных. ORM-объекты и коллекции подходят, когда дальнейший код работает с объектным представлением. Общие операции с коллекциями описаны в статье Коллекции ORM.
Добавить запись
Метод add() принимает массив значений пользовательских полей и возвращает объект с результатом добавления — Bitrix\Main\ORM\Data\AddResult. Объект содержит статус операции, ошибки и идентификатор новой записи. Проверьте isSuccess() перед вызовом getId().
Пример. Добавьте запись и получите ее системный идентификатор.
$addResult = $dataClass::add([
'UF_NAME' => 'Промышленное оборудование',
'UF_CODE' => 'industrial-equipment',
'UF_ACTIVE' => 1,
]);
if (!$addResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $addResult->getErrorMessages())
);
}
$recordId = $addResult->getId();
Динамический класс проверяет пользовательские поля перед сохранением. Например, результат будет содержать ошибку, если обязательное поле не заполнено или значение не прошло проверку типа.
Изменить запись
Метод update() принимает идентификатор записи и массив изменяемых полей. Не передавайте поля, которые не нужно менять.
$updateResult = $dataClass::update($recordId, [
'UF_NAME' => 'Оборудование для производства',
'UF_ACTIVE' => 1,
]);
if (!$updateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $updateResult->getErrorMessages())
);
}
Успешный результат означает, что ORM и пользовательские поля не вернули ошибок. Если нужно убедиться, что изменена существующая запись, получите ее по ID до вызова update() или повторно прочитайте после обновления.
Метод update() не проверяет, изменил ли запись другой процесс. Если запись изменили между чтением и вызовом update(), текущий вызов может перезаписать эти изменения.
Чтобы процессы не обновляли запись одновременно, используйте блокировку в коде приложения. После получения блокировки повторно прочитайте и обновите запись. Такую блокировку нужно предусмотреть в коде проекта: метод update() ее не создает.
Удалить запись
Метод delete() удаляет запись по системному идентификатору и возвращает объект с результатом удаления — Bitrix\Main\ORM\Data\DeleteResult. По объекту можно проверить статус операции и получить ошибки.
$deleteResult = $dataClass::delete($recordId);
if (!$deleteResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $deleteResult->getErrorMessages())
);
}
При удалении записи динамический класс также удаляет значения ее множественных полей. Файлы из файловых пользовательских полей удаляются из файлового хранилища.
Удаление необратимо на уровне API. Если отсутствие записи должно считаться ошибкой, прочитайте ее до вызова delete() и сохраните данные, которые нужны для журнала или восстановления.
Передать значения пользовательских полей
Формат значения зависит от типа и множественности пользовательского поля. Настройки поля определяют проверку и преобразование перед сохранением.
Типы значений и общие настройки собраны в статье Пользовательские поля. Формат полей-привязок и служебных полей справочника приведен в статье Связи и справочники на Highload-блоках.
Одиночные и множественные поля
Одиночное поле принимает одно значение. Множественное поле принимает массив значений. При чтении значение такого поля также возвращается как массив.
$addResult = $dataClass::add([
'UF_NAME' => 'Металлы',
'UF_CODE' => 'metals',
'UF_TAGS' => ['сырье', 'производство'],
]);
При обновлении множественного поля переданный массив заменяет весь прежний набор значений. Передайте пустой массив, чтобы очистить поле.
$updateResult = $dataClass::update($recordId, [
'UF_TAGS' => ['промышленность', 'производство'],
]);
Не изменяйте дополнительное хранилище множественного поля прямыми SQL-запросами. Динамический класс синхронизирует основную запись и множественные значения.
Файловое поле
Для добавления файла подготовьте файловый массив через CFile::MakeFileArray() и передайте его в файловое пользовательское поле. В примере переменная $filePath должна содержать путь к существующему локальному файлу, доступному текущему PHP-процессу.
$file = \CFile::MakeFileArray($filePath);
$addResult = $dataClass::add([
'UF_NAME' => 'Паспорт оборудования',
'UF_CODE' => 'equipment-passport',
'UF_FILE' => $file,
]);
if (!$addResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $addResult->getErrorMessages())
);
}
При чтении файловое поле содержит идентификатор файла, а множественное файловое поле — массив идентификаторов. Для получения пути вызовите CFile::GetPath($fileId), для метаданных — CFile::GetByID($fileId)->Fetch().
Заменить или очистить одиночный файл
Перед заменой получите прежний идентификатор. Добавьте его в ключ old_id нового файлового массива. Пользовательский тип удалит прежний файл и сохранит новый.
$record = $dataClass::getById($recordId)->fetch();
if (!$record)
{
throw new \RuntimeException('Запись не найдена');
}
$newFile = \CFile::MakeFileArray($newFilePath);
$newFile['old_id'] = $record['UF_FILE'];
$updateResult = $dataClass::update($recordId, [
'UF_FILE' => $newFile,
]);
if (!$updateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $updateResult->getErrorMessages())
);
}
Чтобы очистить одиночное поле, передайте файловый массив с прежним идентификатором, признаком del и кодом UPLOAD_ERR_NO_FILE.
$fileToDelete = [
'name' => '',
'type' => '',
'tmp_name' => '',
'error' => UPLOAD_ERR_NO_FILE,
'size' => 0,
'old_id' => $record['UF_FILE'],
'del' => true,
];
$updateResult = $dataClass::update($recordId, [
'UF_FILE' => $fileToDelete,
]);
if (!$updateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $updateResult->getErrorMessages())
);
}
Заменить набор файлов
При обновлении множественного поля передайте операции удаления прежних файлов и массивы новых файлов одним набором. Динамический класс удалит прежние значения и сохранит идентификаторы новых файлов.
$record = $dataClass::getById($recordId)->fetch();
if (!$record)
{
throw new \RuntimeException('Запись не найдена');
}
$files = [];
foreach ((array)$record['UF_FILES'] as $oldFileId)
{
$files[] = [
'name' => '',
'type' => '',
'tmp_name' => '',
'error' => UPLOAD_ERR_NO_FILE,
'size' => 0,
'old_id' => $oldFileId,
'del' => true,
];
}
foreach ($newFilePaths as $newFilePath)
{
$files[] = \CFile::MakeFileArray($newFilePath);
}
$updateResult = $dataClass::update($recordId, [
'UF_FILES' => $files,
]);
if (!$updateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $updateResult->getErrorMessages())
);
}
Удаление файлов происходит до завершения всех шагов обновления. Транзакция базы данных не восстанавливает удаленный файл. До массовой замены подготовьте резервную копию и сначала проверьте сценарий на тестовых данных. Удаление всей записи также удаляет связанные файлы.
Обработать ошибки
Методы add(), update() и delete() возвращают объект результата. Ошибки валидации и обработчиков операций доступны через getErrorMessages().
Исключение и неуспешный результат требуют разной обработки:
-
неуспешный результат означает, что API вернул контролируемые ошибки операции,
-
исключение указывает на ошибку вызова или выполнения, например неизвестное поле в запросе на чтение, некорректный первичный ключ или сбой запроса к базе данных.
Оборачивайте вызов в try только там, где код может обработать исключение или добавить полезный контекст. Результат операции проверяйте всегда.
try
{
$updateResult = $dataClass::update($recordId, $fields);
if (!$updateResult->isSuccess())
{
throw new \RuntimeException(
implode('; ', $updateResult->getErrorMessages())
);
}
}
catch (\Throwable $exception)
{
throw new \RuntimeException(
'Не удалось обновить запись Highload-блока',
0,
$exception
);
}
Не оставляйте неуспешный результат без проверки. Объект результата ORM формирует предупреждение, если код не запросил ошибки.
Операции добавления, изменения и удаления вызывают события динамического ORM-объекта. Обработчики OnBeforeAdd и OnBeforeUpdate могут проверить или изменить данные до сохранения. Обработчики OnBeforeAdd, OnBeforeUpdate и OnBeforeDelete могут добавить ошибку в результат и отменить операцию. Порядок событий и параметры обработчиков описаны в статье События записей и права доступа.
Защитить повторный запуск
Повторный импорт или обработчик очереди не должен создавать дубли. Перед добавлением найдите запись по внешнему коду. Для этого код должен однозначно определять запись и не меняться между запусками. Такой подход подходит только для последовательного запуска.
$record = $dataClass::getRow([
'select' => ['ID'],
'filter' => ['=UF_CODE' => $externalCode],
]);
if ($record === null)
{
$result = $dataClass::add([
'UF_NAME' => $name,
'UF_CODE' => $externalCode,
]);
}
else
{
$result = $dataClass::update($record['ID'], [
'UF_NAME' => $name,
]);
}
if (!$result->isSuccess())
{
throw new \RuntimeException(
implode('; ', $result->getErrorMessages())
);
}
Проверка перед добавлением делает последовательный повторный запуск предсказуемым, но два параллельных процесса могут одновременно не найти запись и создать дубли. Если внешний код должен быть уникальным, добавьте уникальное ограничение в схему базы данных с помощью управляемой миграции и обработайте конфликт добавления.
Если несколько операций зависят друг от друга, заранее определите, как отменить изменения и повторить запуск после ошибки. Например, обработчик события может изменить другую запись, а замена файла — удалить прежний файл до завершения обновления. Транзакция базы данных не восстановит удаленный файл.
Проверить результат
После реализации сценария проверьте его на тестовом Highload-блоке:
-
Добавьте запись и сохраните значение
getId(). -
Получите запись по
IDи сравните одиночные, множественные и файловые поля с исходными данными. -
Обновите только часть полей и убедитесь, что остальные значения сохранились.
-
Передайте новый массив в множественное поле и проверьте полную замену значений.
-
Вызовите операцию с некорректными данными и проверьте обработку
isSuccess()иgetErrorMessages(). -
Удалите запись и убедитесь, что повторное чтение не возвращает данные.
-
Повторите импорт или обработчик и проверьте отсутствие дублей.
Для запросов с большим числом записей отдельно проверьте индексы, размер выборки и план выполнения. Рекомендации собраны в статье Производительность и частые ошибки.