Полный цикл в digital

Понятие сущность и работа с сущностью в 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', ...)
            }
        ];
    }
])

Коды ошибок

Если у поля несколько валидаторов и нужно узнать, какой из них сработал, можно использовать код ошибки. По умолчанию есть два стандартных кода ошибок:

  1. BX_INVALID_VALUE если сработал валидатор
  2. 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')
        ];
    }
}
Заполните форму уже сегодня!
Для начала сотрудничества необходимо заполнить заявку или заказать обратный звонок. В ответ получите коммерческое предложение, которое будет содержать индивидуальную стратегию с учетом требований и поставленных задач
Работаем по будням с 9:00 до 18:00. Заявки, отправленные в выходные, обрабатываем в первый рабочий день до 12:00.
Спасибо, ваш запрос принят и будет обработан!