Концепция ORM
ORM (Object-Relational Mapping) — это технология, которая позволяет работать с базой данных через объекты в коде вместо написания SQL-запросов вручную. Это упрощает взаимодействие с данными и делает код более читаемым и поддерживаемым.
В Bitrix Framework ORM позволяет разработчикам оперировать сущностями. Например, вместо того чтобы вручную писать запрос для добавления записи в таблицу, вы можете создать объект сущности и сохранить его.
Концепция сущностей
Сущность — это таблица в базе данных. Каждая сущность состоит из полей, которые соответствуют колонкам таблицы. Например, сущность «Пользователь» может включать поля: ID, Имя, Фамилия, Пароль, Логин.
Вместо программирования каждой сущности их описывают в формате, который ядро системы воспринимает как конфигурацию.
Пример для каталога книг.
Book
ID int [autoincrement, primary]
ISBN str [match: /[0-9X-]+/]
TITLE str [max_length: 50]
PUBLISH_DATE date
Здесь:
-
ID— уникальный идентификатор книги, который генерируется автоматически, -
ISBN— строка, которая должна соответствовать определенному формату, -
TITLE— название книги, ограниченное 50 символами, -
PUBLISH_DATE— дата публикации.
Система автоматически проверяет корректность и целостность данных на основе описанных правил.
Типизация полей
Для описания сущностей используется PHP. Это обеспечивает гибкость и позволяет использовать возможности языка для настройки сущностей.
Пример определения типов данных:
namespace SomePartner\MyBooksCatalog;
use Bitrix\Main\Entity;
class BookTable extends Entity\DataManager
{
public static function getTableName(): string
{
return 'my_book';
}
public static function getMap(): array
{
return [
new Entity\IntegerField('ID'),
new Entity\StringField('ISBN'),
new Entity\StringField('TITLE'),
new Entity\DateField('PUBLISH_DATE')
];
}
}
Здесь:
-
Класс
BookTableописывает сущность «Книга».Имя класса сущности должно заканчиваться на
Table(например,BookTable). Это требование фреймворка для корректной работы ORM. Основное имяBookв этом пространстве имен зарезервировано для будущего использования, чтобы представлять элементы сущности как объекты, а не массивы. -
Имя таблицы можно задать в методе
getTableName, например,my_book. Если метод не определен, имя таблицы формируется автоматически из неймспейса и названия класса, например,b_somepartner_mybookscatalog_book. -
Каждое поле — это объект класса, который наследуется от
Entity\ScalarField. Например,IntegerField— для целых чисел,StringField— для строк,DateField— для дат. -
Метод
getMap()описывает структуру сущности, возвращая массив полей. Каждое поле — это класс-наследникEntity\ScalarField, работающий с простыми значениями, которые сохраняются в базу данных без изменений.
Типы полей
-
IntegerField— поле для хранения целых чисел.Конструктор:
new Entity\IntegerField('NAME', [ 'size' => 32, // Размер поля в битах (необязательный параметр) ]);Методы настройки:
configureSize($size)— задает размер поля в битах. -
FloatField— поле для хранения чисел с плавающей точкой.Конструктор:
new Entity\FloatField('NAME', [ 'precision' => 10, // Общая точность числа (необязательный параметр) 'scale' => 2, // Количество знаков после запятой (необязательный параметр) ]);Методы настройки:
configurePrecision($precision)— задает общую точность числа.configureScale($scale)— задает количество знаков после запятой. -
DecimalField— поле для хранения чисел с фиксированной точностью.Конструктор:
new Entity\DecimalField('NAME', [ 'precision' => 10, // Общая точность числа (необязательный параметр) 'scale' => 2, // Количество знаков после запятой (необязательный параметр) ]);Методы настройки:
configurePrecision($precision)— задает общую точность числа.configureScale($scale)— задает количество знаков после запятой. -
StringField— поле для хранения строк.Конструктор:
new Entity\StringField('NAME', [ 'format' => '/^[a-zA-Z]+$/', // Регулярное выражение для валидации (необязательный параметр) 'size' => 255, // Максимальная длина строки (необязательный параметр) ]);Методы настройки:
configureFormat($format)— задает регулярное выражение для валидации.configureSize($size)— задает максимальную длину строки. -
TextField— поле для хранения текста большой длины.Конструктор:
new Entity\TextField('NAME', [ 'long' => true, // Указывает, что это поле является длинным текстом (по умолчанию false) ]);Методы настройки:
configureLong($long)— указывает, что поле предназначено для хранения длинного текста. -
DateField— поле для хранения даты.Конструктор:
new Entity\DateField('NAME', [ 'format' => 'Y-m-d', // Формат даты (необязательный параметр) ]);Методы настройки:
configureFormat($format)— задает формат даты. -
DateTimeField— поле для хранения даты и времени.Конструктор:
new Entity\DateTimeField('NAME', [ 'useTimezone' => true, // Использовать временные зоны (по умолчанию true) ]);Методы настройки:
configureUseTimezone($use)— включает или отключает использование временных зон.configureDefaultValueNow()— устанавливает значение по умолчанию как текущую дату и время. -
BooleanField— поле для хранения логических значений.Конструктор:
new Entity\BooleanField('NAME', [ 'values' => ['N', 'Y'], // Маппинг значений (N — false, Y — true) ]);Методы настройки:
configureStorageValues($falseValue, $trueValue)— задает маппинг значений для хранения в БД.booleanizeValue($value)— преобразует любое значение в строгое логическое. -
EnumField— поле для хранения значения из предопределенного списка.Конструктор:
new Entity\EnumField('NAME', [ 'values' => ['VALUE1', 'VALUE2', 'VALUE3'], // Список допустимых значений ]);Методы настройки:
configureValues($values)— задает список допустимых значений. -
ArrayField— поле для хранения массивов данных.Конструктор:
new Entity\ArrayField('NAME', [ 'serializationType' => 'json', // Тип сериализации (json или php) ]);Методы настройки:
configureSerializationJson()— использует JSON для сериализации.configureSerializationPhp()— использует PHP для сериализации.configureSerializeCallback($callback)— задает пользовательский метод сериализации.configureUnserializeCallback($callback)— задает пользовательский метод десериализации. -
CryptoField— поле для хранения зашифрованных данных, которые есть необходимость расшифровывать, например, токены,Конструктор:
new Entity\CryptoField('NAME', [ 'crypto_enabled' => true, // Включить шифрование (по умолчанию true) 'crypto_key' => 'mysecretkey', // Ключ шифрования ]);Методы настройки:
encrypt($data)— шифрует данные перед сохранением.decrypt($data)— расшифровывает данные при чтении.getDefaultKey()— получает ключ шифрования из настроек системы. -
SecretField— поле для хранения зашифрованные данные, которые не должны быть доступны в открытом виде, например пароль.Конструктор:
new Entity\SecretField('NAME', [ 'secret_length' => 16, // Длина секрета ]);Методы настройки:
configureSecretLength($length)— задает длину секрета.getRandomBytes()— генерирует случайные байты для секрета.
Поля рекомендуется называть в верхнем регистре. Они должны быть уникальными в рамках сущности. В конструкторе поля первым параметром идет имя, вторым — настройки.
Первичный ключ, обязательные поля и NULL значения
Первичный ключ (Primary Key) — это уникальный идентификатор записи в таблице. Обычно это поле с автоинкрементом, которое автоматически увеличивается при добавлении новой записи.
В большинстве случаев у сущности есть первичный ключ по одному полю. Указав поле как первичный ключ primary, сущность контролирует вставку данных, не позволяя добавить запись без значения ключа. При обновлении и удалении записи идентифицируются по первичному ключу.
Если значение поля не задается явно, а генерируется базой данных после добавления записи, укажите параметр autocomplete.
new Entity\IntegerField('ID', [
'primary' => true, // Поле является первичным ключом
'autocomplete' => true // Значение генерируется автоматически
]);
Любое поле можно сделать обязательным. Например, чтобы нельзя было добавить книгу без указания ISBN, укажите параметр required.
new Entity\StringField('ISBN', [
'required' => true
]);
Также для полей можно установить разрешать или запрещать NULL значения. За это отвечает параметр nullable.
new Entity\StringField('TITLE', [
'nullable' => true
]);
Значения по умолчанию
Для полей сущностей можно задать значения по умолчанию. Они применяются, если значение для этих полей не указано при добавлении новой записи. Для этого используется параметр default_value.
Параметр default_value может принимать любое значение, которое можно вызвать: имя функции, массив с классом и методом, или анонимную функцию. Это позволяет гибко задавать значения по умолчанию, включая те, которые вычисляются динамически.
Пример установки значения по умолчанию
Рассмотрим пример с каталогом книг, где в поле Дата публикации по умолчанию устанавливается текущий день.
new Entity\DateField('PUBLISH_DATE', [
'default_value' => new Type\Date() // Устанавливаем для поля значение по умолчанию — текущая дата
]);
Теперь, если при добавлении записи не указать дату издания, она будет равна текущему дню.
$result = BookTable::add([
'ISBN' => '978-0321127426', // Указываем ISBN книги
'TITLE' => 'Some new book' // Указываем название книги
]);
Если книги обычно выходят по пятницам, но добавляются позже, можно установить дату последней пятницы.
new Entity\DateField('PUBLISH_DATE', [
'default_value' => function() {
// Определяем дату последней пятницы
$lastFriday = date('Y-m-d', strtotime('last friday'));
return new Type\Date($lastFriday, 'Y-m-d'); // Возвращаем объект даты с последней пятницей
}
]);
Таким образом, использование default_value позволяет автоматически задавать значения для полей, упрощая процесс добавления новых записей.
Маппинг имени колонки
Если вы хотите изменить название колонки в таблице, используйте параметр column_name. Например, в таблице my_book поле ISBN изначально называлось ISBNCODE, и старый код использует это название в SQL-запросах. Чтобы в новом API использовать название ISBN, примените column_name.
new Entity\StringField('ISBN', [
'required' => true,
'column_name' => 'ISBNCODE'
]);
Иногда в одной колонке таблицы могут храниться разные значения. В этом случае можно создать несколько полей сущности с одинаковым column_name.
Выражения ExpressionField
Данные можно не только хранить, но и преобразовывать при выборке. Например, чтобы вместе с датой издания получать возраст книги в днях, не нужно хранить это число в БД и обновлять его ежедневно. Можно вычислить возраст на стороне базы данных.
SELECT DATEDIFF(NOW(), PUBLISH_DATE) AS AGE_DAYS FROM my_book
Здесь DATEDIFF(NOW(), PUBLISH_DATE) — SQL-выражение, которое вычисляет разницу между текущей датой и датой публикации.
В сущности создается виртуальное поле, основанное на SQL-выражении.
new Entity\ExpressionField('AGE_DAYS',
'DATEDIFF(NOW(), %s)', ['PUBLISH_DATE']
);
Первый параметр — имя поля, второй — SQL-выражение с плейсхолдерами вместо полей, третий — массив имен полей в порядке, указанном в выражении. Плейсхолдер %s заменяется на значение поля PUBLISH_DATE.
Используйте плейсхолдеры %s или %1$s, %2$s и так далее, подставляя значения в определенные позиции:
-
%sпоследовательно подставляет значения в том порядке, в котором они переданы, -
%1$sи%2$sявно указывают порядок подстановки значений.
Например, когда в выражении EXPR участвует несколько полей (FIELD_X + FIELD_Y) * FIELD_X, то выражение можно описать так: '(%s + %s) * %s', [FIELD_X, FIELD_Y, FIELD_X]; или так: '(%1$s + %2$s) * %1$s', [FIELD_X, FIELD_Y].
Выражения ExpressionField используются только при выборке данных для фильтрации, группировки и сортировки. Поскольку физически таких колонок в таблице нет, записать значение поля невозможно, и система выдаст исключение.
Пользовательские поля
Пользовательские поля настраиваются через административный интерфейс и не требуют описания в сущности. Для их использования нужно указать идентификатор сущности.
class BookTable extends Entity\DataManager
{
...
public static function getUfId()
{
return 'MY_BOOK';
}
...
}
Метод getUfId() возвращает уникальный идентификатор 'MY_BOOK', который используется для прикрепления пользовательских полей к сущности.

С версии 20.5.200 Главного модуля добавлена поддержка SqlExpression для пользовательских полей в ORM.
Таким образом, можно выбирать и обновлять значения пользовательских полей так же, как и значения стандартных полей сущности.
Валидаторы
Валидаторы используются для проверки данных перед их записью в базу данных. Они помогают убедиться, что данные соответствуют правилам и форматам.
Валидация задается параметром validation в конструкторе поля и представляет собой функцию callback, которая возвращает массив валидаторов. Валидаторы запускаются только тогда, когда действительно необходимо проверить данные, например, перед их сохранением в базу данных. При выборке данных из базы данных валидация не требуется, так как данные уже считаются корректными.
В качестве валидатора можно использовать наследника класса Entity\Validator\Base или любой callable. Объект callable должен вернуть true, текст ошибки или объект Entity\FieldError для собственного кода ошибки.
Стандартные валидаторы
Валидаторы работают при добавлении и обновлении записей. Для проверки данных только при добавлении или обновлении используйте механизм событий.
Рекомендуем использовать стандартные валидаторы:
- BooleanValidator — проверяет, является ли значение допустимым для поля типа boolean.
use Bitrix\Main\ORM\Fields\Validators\BooleanValidator;
(new BooleanField('IS_ACTIVE', [
'required' => true,
]))->addValidator(new BooleanValidator());
- DateValidator — проверяет, является ли значение корректной датой.
use Bitrix\Main\ORM\Fields\Validators\DateValidator;
(new DateField('CREATED_AT', [
'required' => true,
]))->addValidator(new DateValidator());
- EnumValidator — проверяет, находится ли значение в списке допустимых значений для перечисления.
use Bitrix\Main\ORM\Fields\Validators\EnumValidator;
(new EnumField('STATUS', [
'required' => true,
'values' => ['NEW', 'IN_PROGRESS', 'COMPLETED'],
]))->addValidator(new EnumValidator());
- ForeignValidator — проверяет, существует ли значение в связанной таблице (внешний ключ).
use Bitrix\Main\ORM\Fields\Validators\ForeignValidator;
(new IntegerField('GROUP_ID', [
'required' => true,
]))->addValidator(new ForeignValidator(GroupTable::getEntity()->getField('ID')));
- LengthValidator — проверяет длину строкового значения.
use Bitrix\Main\ORM\Fields\Validators\LengthValidator;
(new StringField('NAME', [
'required' => true,
]))->addValidator(new LengthValidator(null, 255));
- RangeValidator — проверяет, находится ли числовое значение в заданном диапазоне.
use Bitrix\Main\ORM\Fields\Validators\RangeValidator;
(new IntegerField('AGE', [
'required' => true,
]))->addValidator(new RangeValidator(18, 100));
- RegExpValidator — проверяет, соответствует ли значение регулярному выражению.
use Bitrix\Main\ORM\Fields\Validators\RegExpValidator;
(new StringField('EMAIL', [
'required' => true,
]))->addValidator(new RegExpValidator('/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'));
- UniqueValidator — проверяет, является ли значение уникальным.
use Bitrix\Main\ORM\Fields\Validators\UniqueValidator;
(new StringField('UNIQUE_CODE', [
'required' => true,
]))->addValidator(new UniqueValidator());
Эти валидаторы нельзя применять к пользовательским полям — проверка значений пользовательских полей настраивается через административный интерфейс.
Пример использования валидаторов
Рассмотрим пример использования валидаторов для проверки поля ISBN в каталоге книг. ISBN — это уникальный идентификатор книги. Сделаем поле ISBN обязательным и зададим шаблон проверки: код поля должен состоять минимум из 13 символов и содержать только цифры и дефисы.
new Entity\StringField('ISBN', [
'required' => true, // Поле обязательно для заполнения
'column_name' => 'ISBNCODE', // Имя столбца в базе данных
'validation' => function() {
return [
new Entity\Validator\RegExp('/[\d-]{13,}/') // Валидатор для проверки формата ISBN
];
}
])
Чтобы проверить, что в ISBN ровно 13 цифр, создайте свой валидатор.
new Entity\StringField('ISBN', [
'required' => true, // Поле обязательно для заполнения
'column_name' => 'ISBNCODE', // Имя столбца в базе данных
'validation' => function() {
return [
function ($value) {
$clean = str_replace('-', '', $value); // Удаляем дефисы из значения
if (preg_match('/^\d{13}$/', $clean)) { // Проверяем, что значение состоит из 13 цифр
return true; // Если условие выполняется, валидация успешна
} else {
return 'Код ISBN должен содержать 13 цифр.'; // Сообщение об ошибке, если условие не выполняется
}
}
];
}
])
Валидатор принимает значение поля $value, но также можно использовать дополнительную информацию.
new Entity\StringField('ISBN', [
'required' => true, // Поле обязательно для заполнения
'column_name' => 'ISBNCODE', // Имя столбца в базе данных
'validation' => function() {
return [
function ($value, $primary, $row, $field) {
// value — значение поля
// primary — массив с первичным ключом, в данном случае [ID => 1]
// row — весь массив данных, переданный в ::add или ::update
// field — объект валидируемого поля, Entity\StringField('ISBN', ...)
}
];
}
])
Коды ошибок
Если у поля несколько валидаторов и нужно узнать, какой из них сработал, можно использовать код ошибки. По умолчанию есть два стандартных кода ошибок:
-
BX_INVALID_VALUE— если сработал валидатор, -
BX_EMPTY_REQUIRED— если не указано обязательное поле.
Например, у поля ISBN последняя цифра — контрольная. Добавим валидатор для ее проверки и обработаем результат.
// описываем валидатор в поле сущности
new Entity\StringField('ISBN', [
'required' => true, // Поле обязательно для заполнения
'column_name' => 'ISBNCODE', // Имя столбца в базе данных
'validation' => function() {
return [
function ($value) {
$clean = str_replace('-', '', $value); // Удаляем дефисы из значения
if (preg_match('/^\d{13}$/', $clean)) { // Проверяем, что значение состоит из 13 цифр
return true; // Если условие выполняется, валидация успешна
} else {
return 'Код ISBN должен содержать 13 цифр.'; // Сообщение об ошибке, если условие не выполняется
}
},
function ($value, $primary, $row, $field) {
// Здесь можно добавить логику проверки контрольной цифры ISBN
// Если контрольная цифра неправильная, возвращаем ошибку
return new Entity\FieldError(
$field, 'Контрольная цифра ISBN не сошлась', 'MY_ISBN_CHECKSUM'
);
}
];
}
])
// выполняем операцию
$result = BookTable::update(...);
if (!$result->isSuccess()) {
// Получаем список ошибок
$errors = $result->getErrors();
foreach ($errors as $error) {
if ($error->getCode() == 'MY_ISBN_CHECKSUM') {
// Обработка ошибки, связанной с нашим валидатором контрольной цифры ISBN
}
}
}
Кеширование
Кеширование ускоряет работу с данными, но в некоторых случаях его нужно отключать. Например, если данные часто изменяются.
С версии 24.100.0 Главного модуля появилась возможность отключать кеширование в ORM-таблицах.
Чтобы отключить кеширование, добавьте в описание таблицы код:
public static function isCacheable(): bool
{
return false;
}
Пример создания сущности
namespace SomePartner\MyBooksCatalog; // Определить пространство имен для организации кода
use Bitrix\Main\Entity; // Подключить класс Entity из Bitrix для работы с сущностями
// Определить класс BookTable, наследующий DataManager
class BookTable extends Entity\DataManager
{
public static function getTableName(): string
{
return 'my_book'; // Возвращает имя таблицы 'my_book'
}
public static function getUfId(): string
{
return 'MY_BOOK'; // Возвращает идентификатор 'MY_BOOK'
}
public static function getMap(): array
{
return [
new Entity\IntegerField('ID', [ // Определить поле 'ID' как целочисленного
'primary' => true, // Указать, что это первичный ключ
'autocomplete' => true // Автоматически увеличивать значения (автоинкремент)
]),
new Entity\StringField('ISBN', [ // Определить поле 'ISBN' как строковое
'required' => true, // Поле обязательно для заполнения
'column_name' => 'ISBNCODE' // Имя колонки в базе данных 'ISBNCODE'
]),
new Entity\StringField('TITLE'), // Определить поле 'TITLE' как строковое
new Entity\DateField('PUBLISH_DATE') // Определить поле 'PUBLISH_DATE' как дату
];
}
}
Метод getMap используется для получения первичной конфигурации сущности. Чтобы получить актуальный список полей, используйте BookTable::getEntity()->getFields().
Код сущности нужно сохранить в файле /local/modules/somepartner.mybookscatalog/lib/book.php. Система автоматически подключит файл при вызовах класса BookTable.