Концепция 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.