Перейти к содержанию

Концепция 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 — поле для коротких строк.

По умолчанию хранит строку до 255 символов. Параметр size задает максимальную длину: значение может быть меньше или больше 255.

Конструктор:

new Entity\StringField('NAME', [
    'format' => '/^[a-zA-Z]+$/', // Регулярное выражение для валидации (необязательный параметр)
    'size' => 255, // Максимальная длина строки (необязательный параметр)
]);

Методы настройки:

configureFormat($format) — задает регулярное выражение для валидации.

configureSize($size) — задает максимальную длину строки.

  • TextField — поле для текста, который не нужно ограничивать размером StringField.

Параметр long определяет тип хранения. Если параметр не указан или равен false, поле хранится как text. Если указать long => true, поле хранится как longtext.

Конструктор:

new Entity\TextField('NAME', [
    'long' => true, // Использовать longtext вместо text (по умолчанию false)
]);

Примечание

text хранит до 65 535 байт данных.

longtext хранит до 4 294 967 295 байт данных.

Это лимиты в байтах, а не гарантированное количество символов.

Фактическое число символов зависит от кодировки: в многобайтовых кодировках один символ может занимать несколько байт.

Например, если каждый символ занимает 2 байта, в text поместится примерно 32 000 символов. Если каждый символ занимает 4 байта, — примерно 16 000 символов.

Методы настройки:

configureLong($long) — переключает тип хранения:

  • configureLong(true) использует longtext,

  • configureLong(false) оставляет text.

  • 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.