Понятие сущность и работа с сущностью в ORM Битрикс
Сущность — это таблица в базе данных. Каждая сущность состоит из полей, которые соответствуют колонкам таблицы. Например, сущность «Пользователь» может включать поля: ID, Имя, Фамилия, Пароль, Логин.
Вместо программирования каждой сущности их описывают в формате, который ядро системы воспринимает как конфигурацию.
Пример для каталога книг:
Book
// ID уникальный идентификатор книги, который генерируется автоматически
ID int [autoincrement, primary]
// ISBN строка, которая должна соответствовать определенному формату
ISBN str [match: /[0-9X-]+/]
// TITLE название книги, ограниченное 50 символами
TITLE str [max_length: 50]
// PUBLISH_DATE дата публикации
PUBLISH_DATE 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
. Это требование фреймворка для корректной работы 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', [
// Указывает, что это поле является длинным текстом (по умолчанию false)
'long' => true,
]);
Методы настройки:
configureLong($long)
указывает, что поле предназначено для хранения длинного текста
DateField поле для хранения даты
Конструктор:
new Entity\DateField('NAME', [
// Формат даты (необязательный параметр)
'format' => 'Y-m-d',
]);
Методы настройки:
configureFormat($format)
задает формат даты
DateTimeField поле для хранения даты и времени
Конструктор:
new Entity\DateTimeField('NAME', [
// Использовать временные зоны (по умолчанию true)
'useTimezone' => true,
]);
Методы настройки:
configureUseTimezone($use)
включает или отключает использование временных зонconfigureDefaultValueNow()
устанавливает значение по умолчанию как текущую дату и время
BooleanField поле для хранения логических значений
Конструктор:
new Entity\BooleanField('NAME', [
// Маппинг значений (N — false, Y — true)
'values' => ['N', 'Y'],
]);
Методы настройки:
configureStorageValues($falseValue, $trueValue)
задает маппинг значений для хранения в БДbooleanizeValue($value)
преобразует любое значение в строгое логическое
EnumField поле для хранения значения из предопределенного списка
Конструктор:
new Entity\EnumField('NAME', [
// Список допустимых значений
'values' => ['VALUE1', 'VALUE2', 'VALUE3'],
]);
Методы настройки:
configureValues($values)
задает список допустимых значений
ArrayField поле для хранения массивов данных
Конструктор:
new Entity\ArrayField('NAME', [
// Тип сериализации (json или php)
'serializationType' => 'json',
]);
Методы настройки:
configureSerializationJson()
использует JSON для сериализацииconfigureSerializationPhp()
использует PHP для сериализацииconfigureSerializeCallback($callback)
задает пользовательский метод сериализацииconfigureUnserializeCallback($callback)
задает пользовательский метод десериализации
CryptoField— поле для хранения зашифрованных данных, которые есть необходимость расшифровывать, например, токены
Конструктор:
new Entity\CryptoField('NAME', [
// Включить шифрование (по умолчанию true)
'crypto_enabled' => true,
// Ключ шифрования
'crypto_key' => 'mysecretkey',
]);
Методы настройки:
encrypt($data)
шифрует данные перед сохранениемdecrypt($data)
расшифровывает данные при чтенииgetDefaultKey()
получает ключ шифрования из настроек системы
SecretField поле для хранения зашифрованные данные, которые не должны быть доступны в открытом виде, например пароль
Конструктор:
new Entity\SecretField('NAME', [
// Длина секрета
'secret_length' => 16,
]);
Методы настройки:
configureSecretLength($length)
задает длину секретаgetRandomBytes()
генерирует случайные байты для секрета
Первичный ключ Primary Key
Первичный ключ (Primary Key) — это уникальный идентификатор записи в таблице. Обычно это поле с автоинкрементом, которое автоматически увеличивается при добавлении новой записи
В большинстве случаев у сущности есть первичный ключ по одному полю. Указав поле как первичный ключ primary
, сущность контролирует вставку данных, не позволяя добавить запись без значения ключа. При обновлении и удалении записи идентифицируются по первичному ключу.
Если значение поля не задается явно, а генерируется базой данных после добавления записи, укажите параметр autocomplete
.
new Entity\IntegerField('ID', [
// Поле является первичным ключом
'primary' => true,
// Значение генерируется автоматически
'autocomplete' => true
]);
Обязательное поле
Любое поле можно сделать обязательным. Например, чтобы нельзя было добавить книгу без указания ISBN, укажите параметр required
.
new Entity\StringField('ISBN', [
'required' => true
]);
Разрешить значение NULL
Для полей можно установить разрешать или запрещать 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' // Указываем название книги
]);
Маппинг имени колонки
Если вы хотите изменить название колонки в таблице, используйте параметр column_name
. Например, в таблице my_book
поле ISBN
изначально называлось ISBNCODE
и старый код использует это название в SQL-запросах. Чтобы в новом API использовать название ISBN
, примените column_name
.
new Entity\StringField('ISBN', [
'required' => true,
'column_name' => 'ISBNCODE'
]);
Выражения 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. Таким образом, можно выбирать и обновлять значения пользовательских полей так же, как и значения стандартных полей сущности.
Кеширование
Кеширование ускоряет работу с данными, но в некоторых случаях его нужно отключать. Например, если данные часто изменяются.
С версии 24.100.0 Главного модуля появилась возможность отключать кеширование в ORM-таблицах.
Чтобы отключить кеширование, добавьте в описание таблицы код:
public static function isCacheable(): bool
{
return false;
}
Валидаторы
Валидаторы используются для проверки данных перед их записью в базу данных. Они помогают убедиться, что данные соответствуют правилам и форматам.
Валидация задается параметром validation
в конструкторе поля и представляет собой функцию callback
, которая возвращает массив валидаторов. Валидаторы запускаются только тогда, когда действительно необходимо проверить данные, например, перед их сохранением в базу данных. При выборке данных из базы данных валидация не требуется, так как данные уже считаются корректными.
В качестве валидатора можно использовать наследника класса Entity\Validator\Base
или любой callable
. Объект callable
должен вернуть true
, текст ошибки или объект Entity\FieldError
для собственного кода ошибки.
Валидаторы работают при добавлении и обновлении записей. Для проверки данных только при добавлении или обновлении используйте механизм событий.
Стандартные валидаторы
DateValidato
проверяет, является ли значение корректной датой:
(new DateField('CREATED_AT', [
'required' => true,
]))->addValidator(new DateValidator());
BooleanValidator проверяет, является ли значение допустимым для поля типа boolean
:
(new BooleanField('IS_ACTIVE', [
'required' => true,
]))->addValidator(new BooleanValidator());
DateValidator
проверяет, является ли значение корректной датой:
(new DateField('CREATED_AT', [
'required' => true,
]))->addValidator(new DateValidator());
EnumValidator
проверяет, находится ли значение в списке допустимых значений для перечисления:
(new EnumField('STATUS', [
'required' => true,
'values' => ['NEW', 'IN_PROGRESS', 'COMPLETED'],
]))->addValidator(new EnumValidator());
ForeignValidator
проверяет, существует ли значение в связанной таблице (внешний ключ):
(new IntegerField('GROUP_ID', [
'required' => true,
]))->addValidator(new Foreign(GroupTable::getEntity()->getField('ID')));
LengthValidator
проверяет длину строкового значения:
(new StringField('NAME', [
'required' => true,
]))->addValidator(new Length(null, 255));
RangeValidator
проверяет, находится ли числовое значение в заданном диапазоне:
(new IntegerField('AGE', [
'required' => true,
]))->addValidator(new Range(18, 100));
RegExpValidator
проверяет, соответствует ли значение регулярному выражению:
(new StringField('EMAIL', [
'required' => true,
]))->addValidator(new RegExp('/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'));
UniqueValidator
проверяет, является ли значение уникальным:
(new StringField('UNIQUE_CODE', [
'required' => true,
]))->addValidator(new Unique());
Эти валидаторы нельзя применять к пользовательским полям — проверка значений пользовательских полей настраивается через административный интерфейс.
Пример использования валидаторов
Рассмотрим пример использования валидаторов для проверки поля ISBN в каталоге книг. ISBN — это уникальный идентификатор книги. Сделаем поле ISBN обязательным и зададим шаблон проверки: код поля должен состоять минимум из 13 символов и содержать только цифры и дефисы.
new Entity\StringField('ISBN', [
// Поле обязательно для заполнения
'required' => true,
// Имя столбца в базе данных
'column_name' => 'ISBNCODE',
'validation' => function() {
return [
// Валидатор для проверки формата ISBN
new Entity\Validator\RegExp('/[\d-]{13,}/')
];
}
])
Чтобы проверить, что в ISBN ровно 13 цифр, создайте свой валидатор.
new Entity\StringField('ISBN', [
// Поле обязательно для заполнения
'required' => true,
// Имя столбца в базе данных
'column_name' => 'ISBNCODE',
'validation' => function() {
return [
function ($value) {
// Удаляем дефисы из значения
$clean = str_replace('-', '', $value);
// Проверяем, что значение состоит из 13 цифр
if (preg_match('/^\d{13}$/', $clean)) {
// Если условие выполняется, валидация успешна
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);
// Проверяем, что значение состоит из 13 цифр
if (preg_match('/^\d{13}$/', $clean)) {
// Если условие выполняется, валидация успешна
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
}
}
}
Пример создания сущности
// Определяем пространство имен для организации кода
namespace SomePartner\MyBooksCatalog;
// Подключить класс Entity из Bitrix для работы с сущностями
use Bitrix\Main\Entity;
// Определить класс BookTable, наследующий DataManager
class BookTable extends Entity\DataManager
{
public static function getTableName(): string
{
// Возвращает имя таблицы 'my_book'
return 'my_book';
}
public static function getUfId(): string
{
// Возвращает идентификатор 'MY_BOOK'
return 'MY_BOOK';
}
public static function getMap(): array
{
return [
// Определить поле 'ID' как целочисленного
new Entity\IntegerField('ID', [
// Указать, что это первичный ключ
'primary' => true,
// Автоматически увеличивать значения (автоинкремент)
'autocomplete' => true
]),
// Определить поле 'ISBN' как строковое
new Entity\StringField('ISBN', [
// Поле обязательно для заполнения
'required' => true,
// Имя колонки в базе данных 'ISBNCODE'
'column_name' => 'ISBNCODE'
]),
// Определить поле 'TITLE' как строковое
new Entity\StringField('TITLE'),
// Определить поле 'PUBLISH_DATE' как дату
new Entity\DateField('PUBLISH_DATE')
];
}
}