Active Record Yii 2
Active Record Yii 2

Active Record

Active Record обеспечивает объектно-ориентированный интерфейс для доступа и манипулирования данными, хранящимися в базах данных. Класс Active Record соответствует таблице в базе данных, объект Active Record соответствует строке этой таблицы, а атрибут объекта Active Record представляет собой значение отдельного столбца строки. Вместо непосредственного написания SQL-выражений вы сможете получать доступ к атрибутам Active Record и вызывать методы Active Record для доступа и манипулирования данными, хранящимися в таблицах базы данных.



Для примера предположим, что Customer - это класс Active Record, который сопоставлен с таблицей customer, а name - столбец в таблице customer. Тогда вы можете написать следующий код для вставки новой строки в таблицу customer:

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

Вышеприведённый код аналогичен использованию следующего SQL-выражения в MySQL, которое менее интуитивно, потенциально может вызвать ошибки и даже проблемы совместимости, если вы используете различные виды баз данных:

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
    ':name' => 'Qiang',
])->execute();

Yii поддерживает работу с Active Record для следующих реляционных баз данных:

  • MySQL 4.1 и выше: посредством [[yii\db\ActiveRecord]]
  • PostgreSQL 7.3 и выше: посредством [[yii\db\ActiveRecord]]
  • SQLite 2 и 3: посредством [[yii\db\ActiveRecord]]
  • Microsoft SQL Server 2008 и выше: посредством [[yii\db\ActiveRecord]]
  • Oracle: посредством [[yii\db\ActiveRecord]]
  • CUBRID 9.3 и выше: посредством [[yii\db\ActiveRecord]] (Имейте ввиду, что вследствие бага в PDO-расширении для CUBRID, заключение значений в кавычки не работает, поэтому необходимо использовать CUBRID версии 9.3 как на клиентской стороне, так и на сервере)
  • Sphinx: посредством [[yii\sphinx\ActiveRecord]], потребуется расширение yii2-sphinx
  • ElasticSearch: посредством [[yii\elasticsearch\ActiveRecord]], потребуется расширение yii2-elasticsearch

Кроме того Yii поддерживает использование Active Record со следующими NoSQL базами данных:

  • Redis 2.6.12 и выше: посредством [[yii\redis\ActiveRecord]], потребуется расширение yii2-redis
  • MongoDB 1.3.0 и выше: посредством [[yii\mongodb\ActiveRecord]], потребуется расширение yii2-mongodb

В этом руководстве мы в основном будем описывать использование Active Record для реляционных баз данных. Однако большая часть этого материала также применима при использовании Active Record с NoSQL базами данных.

Объявление классов Active Record

Для начала объявите свой собственный класс, унаследовав класс [[yii\db\ActiveRecord]].

Настройка имени таблицы

По умолчанию каждый класс Active Record ассоциирован с таблицей в базе данных. Метод [[yii\db\ActiveRecord::tableName()|tableName()]] получает имя таблицы из имени класса с помощью [[yii\helpers\Inflector::camel2id()]]. Если таблица не названа соответственно, вы можете переопределить данный метод.

Также может быть применён [[yii\db\Connection::$tablePrefix|tablePrefix]] по умолчанию. Например, если [[yii\db\Connection::$tablePrefix|tablePrefix]] задан как tbl_, Customer преобразуется в tbl_customer, а OrderItem в tbl_order_item.

Если имя таблицы указано в формате {{%TableName}}, символ % заменяется префиксом. Например, , {{%post}} становится {{tbl_post}}. Фигуриные скобки используются для экранирования в SQL-запросах.

В нижеследующем примере мы объявляем класс Active Record с названием Customer для таблицы customer.

namespace app\models;

use yii\db\ActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
    
    /**
     * @return string название таблицы, сопоставленной с этим ActiveRecord-классом.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Классы Active record называются "моделями"

Объекты Active Record являются моделями. Именно поэтому мы обычно задаём классам Active Record пространство имён app\models (или другое пространство имён, предназначенное для моделей).

Т.к. класс [[yii\db\ActiveRecord]] наследует класс [[yii\base\Model]], он обладает всеми возможностями моделей, такими как атрибуты, правила валидации, способы сериализации данных и т.д.

Подключение к базам данных

По умолчанию Active Record для доступа и манипулирования данными БД использует компонент приложения db в качестве компонента [[yii\db\Connection|DB connection]]. Как сказано в разделе Объекты доступа к данным (DAO), вы можете настраивать компонент db на уровне конфигурации приложения как показано ниже:

return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

Если вы хотите использовать для подключения к базе данных другой компонент подключения, отличный от db, вам нужно переопределить метод [[yii\db\ActiveRecord::getDb()|getDb()]]:

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // использовать компонент приложения "db2"
        return \Yii::$app->db2;  
    }
}

Получение данных

После объявления класса Active Record вы можете использовать его для получения данных из соответствующей таблицы базы данных. Этот процесс, как правило, состоит из следующих трёх шагов:

  1. Создать новый объект запроса вызовом метода [[yii\db\ActiveRecord::find()]];
  2. Настроить объект запроса вызовом методов построения запросов;
  3. Вызвать один из методов получения данных для извлечения данных в виде объектов Active Record.

Как вы могли заметить, эти шаги очень похожи на работу с построителем запросов. Различие лишь в том, что для создания объекта запроса вместо оператора new используется метод [[yii\db\ActiveRecord::find()]], возвращающий новый объект запроса, являющийся представителем класса [[yii\db\ActiveQuery]].

Ниже приведено несколько примеров использования Active Query для получения данных:

// возвращает покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// возвращает всех активных покупателей, сортируя их по идентификаторам
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// возвращает количество активных покупателей
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// возвращает всех покупателей массивом, индексированным их идентификаторами
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

В примерах выше $customer - это объект класса Customer, в то время как $customers - это массив таких объектов. Все эти объекты заполнены данными таблицы customer.

Info: Т.к. класс [[yii\db\ActiveQuery]] наследует [[yii\db\Query]], вы можете использовать в нём все методы построения запросов и все методы класса Query как описано в разделе Построитель запросов.

Т.к. извлечение данных по первичному ключу или значениям отдельных столбцов достаточно распространённая задача, Yii предоставляет два коротких метода для её решения:

  • [[yii\db\ActiveRecord::findOne()]]: возвращает один объект Active Record, заполненный первой строкой результата запроса.
  • [[yii\db\ActiveRecord::findAll()]]: возвращает массив объектов Active Record, заполненных всеми полученными результатами запроса.

Оба метода могут принимать параметры в одном из следующих форматов:

  • скалярное значение: значение интерпретируется как первичный ключ, по которому следует искать. Yii прочитает информацию о структуре базы данных и автоматически определит, какой столбец таблицы содержит первичные ключи.
  • массив скалярных значений: массив интерпретируется как набор первичных ключей, по которым следует искать.
  • ассоциативный массив: ключи массива интерпретируются как названия столбцов, а значения - как содержимое столбцов, которое следует искать. За подробностями вы можете обратиться к разделу Hash Format

Нижеследующий код демонстрирует, каким образом эти методы могут быть использованы:

// возвращает покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// возвращает покупателей с идентификаторами 100, 101, 123 и 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// возвращает активного покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// возвращает всех неактивных покупателей
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

Warning: Если вам нужно передать в эти методы данные, полученные от пользователя, убедитесь что передаваемое значение – это скаляр, а если необходимо указать условия в формате массива – убедитесь, что пользовательские данные не могут изменить структуру этого массива.

// yii\web\Controller гарантирует, что $id будет скаляром
public function actionView($id)
{
    $model = Post::findOne($id);
    // ...
}

// явное указание имени столбца для поиска гарантирует поиск по столбцу `id`,
// и возвращение одной записи как для массива, так и для скаляра в принятом от пользователя поле `id` 
$model = Post::findOne(['id' => Yii::$app->request->get('id')]);

// НЕ используйте этот код! Пользователь может передать в параметр `id` массив
// и осуществить поиск по имени столбца, которое не должно быть использовано для поиска по логике вашего приложения.
$model = Post::findOne(Yii::$app->request->get('id'));

Note: Ни метод [[yii\db\ActiveRecord::findOne()]], ни [[yii\db\ActiveQuery::one()]] не добавляет условие LIMIT 1 к генерируемым SQL-запросам. Если ваш запрос может вернуть много строк данных, вы должны вызвать метод limit(1) явно в целях улучшения производительности, например: Customer::find()->limit(1)->one().

Помимо использования методов построения запросов вы можете также писать запросы на "чистом" SQL для получения данных и заполнения ими объектов Active Record. Вы можете делать это посредством метода [[yii\db\ActiveRecord::findBySql()]]:

// возвращает всех неактивных покупателей
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

Не используйте дополнительные методы построения запросов после вызова метода [[yii\db\ActiveRecord::findBySql()|findBySql()]], т.к. они будут проигнорированы.

Доступ к данным

Как сказано выше, получаемые из базы данные заполняют объекты Active Record и каждая строка результата запроса соответствует одному объекту Active Record. Вы можете получить доступ к значениям столбцов с помощью атрибутов этих объектов. Например так:

// "id" и "email" - названия столбцов в таблице "customer"
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

Note: Атрибуты объекта Active Record названы в соответствии с названиями столбцов связной таблицы с учётом регистра. Yii автоматически объявляет для каждого столбца связной таблицы атрибут в Active Record. Вы НЕ должны переопределять какие-либо из этих атрибутов.

Атрибуты Active Record названы в соответствии с именами столбцов таблицы. Если столбцы вашей таблицы именуются через нижнее подчёркивание, то может оказаться, что вам придётся писать PHP-код вроде этого: $customer->first_name - в нём будет использоваться нижнее подчёркивание для разделения слов в названиях атрибутов. Если вы обеспокоены единообразием стиля кодирования, вам придётся переименовать столбцы вашей таблицы соответствующим образом (например, назвать столбцы в стиле camelCase).

Преобразование данных

Часто бывает так, что данные вводятся и/или отображаются в формате, который отличается от формата их хранения в базе данных. Например, в базе данных вы храните дни рождения покупателей в формате UNIX timestamp (что, кстати говоря, не является хорошим дизайном), в то время как во многих случаях вы хотите манипулировать днями рождения в виде строк формата 'ДД.ММ.ГГГГ'. Для достижения этой цели, вы можете объявить методы преобразования данных в ActiveRecord-классе Customer как показано ниже:

class Customer extends ActiveRecord
{
    // ...

    public function getBirthdayText()
    {
        return date('d.m.Y', $this->birthday);
    }
    
    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

Теперь в своём PHP коде вместо доступа к $customer->birthday, вы сможете получить доступ к $customer->birthdayText, что позволить вам вводить и отображать дни рождения покупателей в формате 'ДД.ММ.ГГГГ'.

Tip: Вышеприведённый пример демонстрирует общий способ преобразования данных в различные форматы. Если вы работаете с датами и временем, вы можете использовать DateValidator и [[yii\jui\DatePicker|DatePicker]], которые проще в использовании и являются более мощными инструментами.

Получение данных в виде массива

Несмотря на то, что получение данных в виде Active Record объектов является удобным и гибким, этот способ не всегда подходит при получении большого количества данных из-за больших накладных расходов памяти. В этом случае вы можете получить данные в виде PHP-массива, используя перед выполнением запроса метод [[yii\db\ActiveQuery::asArray()|asArray()]]:

// возвращает всех покупателей
// каждый покупатель будет представлен в виде ассоциативного массива
$customers = Customer::find()
    ->asArray()
    ->all();

Note: В то время как этот способ бережёт память и улучшает производительность, он ближе к низкому слою абстракции базы данных и вы потеряете многие возможности Active Record. Важное отличие заключается в типах данных значений столбцов. Когда вы получаете данные в виде объектов Active Record, значения столбцов автоматически приводятся к типам, соответствующим типам столбцов; с другой стороны, когда вы получаете данные в массивах, значения столбцов будут строковыми (до тех пор, пока они являются результатом работы PDO-слоя без какой-либо обработки), несмотря на настоящие типы данных соответствующих столбцов.

Пакетное получение данных

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

// получить 10 покупателей одновременно
foreach (Customer::find()->batch(10) as $customers) {
    // $customers - это массив, в котором находится 10 или меньше объектов класса Customer
}

// получить одновременно десять покупателей и перебрать их одного за другим
foreach (Customer::find()->each(10) as $customer) {
    // $customer - это объект класса Customer
}

// пакетная выборка с жадной загрузкой
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer - это объект класса Customer
}

Сохранение данных

Используя Active Record, вы легко можете сохранить данные в базу данных, осуществив следующие шаги:

  1. Подготовьте объект Active Record;
  2. Присвойте новые значения атрибутам Active Record;
  3. Вызовите метод [[yii\db\ActiveRecord::save()]] для сохранения данных в базу данных.

Например:

// вставить новую строку данных
$customer = new Customer();
$customer->name = 'James';
$customer->email = '[email protected]';
$customer->save();

// обновить имеющуюся строку данных
$customer = Customer::findOne(123);
$customer->email = '[email protected]';
$customer->save();

Метод [[yii\db\ActiveRecord::save()|save()]] может вставить или обновить строку данных в зависимости от состояния Active Record объекта. Если объект создан с помощью оператора new, вызов метода [[yii\db\ActiveRecord::save()|save()]] приведёт к вставке новой строки данных; если же объект был получен с помощью запроса на получение данных, вызов [[yii\db\ActiveRecord::save()|save()]] обновит строку таблицы, соответствующую объекту Active Record.

Вы можете различать два состояния Active Record объекта с помощью проверки значения его свойства [[yii\db\ActiveRecord::isNewRecord|isNewRecord]]. Это свойство также используется внутри метода [[yii\db\ActiveRecord::save()|save()]] как показано ниже:

public function save($runValidation = true, $attributeNames = null)
{
    if ($this->getIsNewRecord()) {
        return $this->insert($runValidation, $attributeNames);
    } else {
        return $this->update($runValidation, $attributeNames) !== false;
    }
}

Tip: Вы можете вызвать [[yii\db\ActiveRecord::insert()|insert()]] или [[yii\db\ActiveRecord::update()|update()]] непосредственно, чтобы вставить или обновить строку данных в таблице.

Валидация данных

Т.к. класс [[yii\db\ActiveRecord]] наследует класс [[yii\base\Model]], он обладает такими же возможностями валидации данных. Вы можете объявить правила валидации переопределив метод [[yii\db\ActiveRecord::rules()|rules()]] и осуществлять валидацию данных посредством вызовов метода [[yii\db\ActiveRecord::validate()|validate()]].

Когда вы вызываете метод [[yii\db\ActiveRecord::save()|save()]], по умолчанию он автоматически вызывает метод [[yii\db\ActiveRecord::validate()|validate()]]. Только после успешного прохождения валидации происходит сохранение данных; в ином случае метод [[yii\db\ActiveRecord::save()|save()]] просто возвращает false, и вы можете проверить свойство [[yii\db\ActiveRecord::errors|errors]] для получения сообщений об ошибках валидации.

Tip: Если вы уверены, что ваши данные не требуют валидации (например, данные пришли из доверенного источника), вы можете вызвать save(false), чтобы пропустить валидацию.

Массовое присваивание

Как и обычные модели, объекты Active Record тоже обладают возможностью массового присваивания. Как будет показано ниже, используя эту возможность, вы можете одним PHP выражением присвоить значения множества атрибутов Active Record объекту. Запомните однако, что только безопасные атрибуты могут быть массово присвоены.

$values = [
    'name' => 'James',
    'email' => '[email protected]',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

Обновление счётчиков

Распространённой задачей является инкремент или декремент столбца в таблице базы данных. Назовём такие столбцы столбцами-счётчиками. Вы можете использовать метод [[yii\db\ActiveRecord::updateCounters()|updateCounters()]] для обновления одного или нескольких столбцов-счётчиков. Например:

$post = Post::findOne(100);

// UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
$post->updateCounters(['view_count' => 1]);

Note: Если вы используете метод [[yii\db\ActiveRecord::save()]] для обновления столбца-счётчика, вы можете прийти к некорректному результату, т.к. вполне вероятно, что этот же счётчик был сохранён сразу несколькими запросами, которые читают и записывают этот же столбец-счётчик.

Dirty-атрибуты

Когда вы вызываете [[yii\db\ActiveRecord::save()|save()]] для сохранения Active Record объекта, сохраняются только dirty-атрибуты. Атрибут считается dirty-атрибутом, если его значение было изменено после чтения из базы данных или же он был сохранён в базу данных совсем недавно. Заметьте, что валидация данных осуществляется независимо от того, имеются ли dirty-атрибуты в объекте Active Record или нет.

Active Record автоматически поддерживает список dirty-атрибутов. Это достигается за счёт хранения старых значений атрибутов и сравнения их с новыми. Вы можете вызвать метод [[yii\db\ActiveRecord::getDirtyAttributes()]] для получения текущего списка dirty-атрибутов. Вы также можете вызвать [[yii\db\ActiveRecord::markAttributeDirty()]], чтобы явно пометить атрибут в качестве dirty-атрибута.

Если вам нужны значения атрибутов, какими они были до их изменения, вы можете вызвать [[yii\db\ActiveRecord::getOldAttributes()|getOldAttributes()]] или [[yii\db\ActiveRecord::getOldAttribute()|getOldAttribute()]].

Note: Сравнение старых и новых значений будет осуществлено с помощью оператора ===, так что значение будет считаться dirty-значением даже в том случае, если оно осталось таким же, но изменило свой тип. Это часто происходит, когда модель получает пользовательский ввод из HTML-форм, где каждое значение представлено строкой. Чтобы убедиться в корректности типа данных, например для целых значений, вы можете применить фильтрацию данных: ['attributeName', 'filter', 'filter' => 'intval'].

Значения атрибутов по умолчанию

Некоторые столбцы ваших таблиц могут иметь значения по умолчанию, объявленные в базе данных. Иногда вы можете захотеть предварительно заполнить этими значениями вашу веб-форму, которая соответствует Active Record объекту. Чтобы избежать повторного указания этих значений, вы можете вызвать метод [[yii\db\ActiveRecord::loadDefaultValues()|loadDefaultValues()]] для заполнения соответствующих Active Record атрибутов значениями по умолчанию, объявленными в базе данных:

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz получит значение по умолчанию, которое было указано при объявлении столбца "xyz"

Приведение типов атрибутов

При заполнении результатами запроса [[yii\db\ActiveRecord]] производит автоматическое приведение типов для значений атрибутов на основе информации из схемы базы данных. Это позволяет данным, полученным из колонки таблицы объявленной как целое, заноситься в экземпляр ActiveRecord как значение целого типа PHP, булево как булево и т.д. Однако, механизм приведения типов имеет несколько ограничений:

  • Числа с плавающей точкой не будут обработаны, а будут представленны как строки, в противном случае они могут потерять точность.
  • Конвертация целых чисел зависит от разрядности используемой операционной системы. В частности: значения колонок, объявленных как 'unsigned integer' или 'big integer' будут приведены к целому типу PHP только на 64-х разрядных системах, в то время как на 32-х разрядных - они будут представленны как строки.

Имейте в виду, что преобразование типов производиться только в момент заполнения экземпляра ActiveRecord данными из результата запроса. При заполнении данных из HTTP запроса или непосредственно через механизм доступа к полям - автоматическая конвертация не производтся. Схема таблицы базы данных также используется при построении SQL запроса для сохранения данных ActiveRecord, обеспечивая соответсвие типов связываемых параметров в запросе. Однако, над атрибутами объекта ActiveRecord не будет производиться приведение типов в процессе сохранения.

Совет: вы можете использовать поведение [[yii\behaviors\AttributeTypecastBehavior]] для того, чтобы производить приведение типов для ActiveRecord во время валидации или сохранения.

Начиная с 2.0.14, Yii ActiveRecord поддерживает сложные типы данных, такие как JSON или многомерные массивы.

JSON в MySQL и PostgreSQL

После заполнения данных, значение из столбца JSON будет автоматически декодировано из JSON в соответствии со стандартными правилами декодирования JSON.

Чтобы сохранить значение атрибута в столбец JSON, ActiveRecord автоматически создаст объект [[yii\db\JsonExpression|JsonExpression]], который будет закодирован в строку JSON на уровне QueryBuilder.

Массивы в PostgreSQL

После заполнения данных значение из столбца Array будет автоматически декодировано из нотации PgSQL в объект [[yii\db\ArrayExpression|ArrayExpression]]. Он реализует интерфейс PHP ArrayAccess, так что вы можете использовать его в качестве массива, или вызвать ->getValue (), чтобы получить сам массив.

Чтобы сохранить значение атрибута в столбец массива, ActiveRecord автоматически создаст объект [[yii\db\Array Expression|ArrayExpression]], который будет закодирован QueryBuilder в строковое представление массива PgSQL.

Можно также использовать условия для столбцов JSON:

$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])

Дополнительные сведения о системе построения выражений см. Query Builder – добавление пользовательских условий и выражений

Обновление нескольких строк данных

Методы, представленные выше, работают с отдельными Active Record объектами, инициируя вставку или обновление данных для отдельной строки таблицы. Вместо них для обновления нескольких строк одновременно можно использовать метод [[yii\db\ActiveRecord::updateAll()|updateAll()]], который является статическим.

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

Подобным образом можно использовать метод [[yii\db\ActiveRecord::updateAllCounters()|updateAllCounters()]] для обновления значений столбцов-счётчиков в нескольких строках одновременно.

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

Удаление данных

Для удаления одной отдельной строки данных сначала получите Active Record объект, соответствующий этой строке, а затем вызовите метод [[yii\db\ActiveRecord::delete()]].

$customer = Customer::findOne(123);
$customer->delete();

Вы можете вызвать [[yii\db\ActiveRecord::deleteAll()]] для удаления всех или нескольких строк данных одновременно. Например:

Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);

Note: будьте очень осторожны, используя метод [[yii\db\ActiveRecord::deleteAll()|deleteAll()]], потому что он может полностью удалить все данные из вашей таблицы, если вы сделаете ошибку при указании условий удаления.

Жизненные циклы Active Record

Важно понимать как устроены жизненные циклы Active Record при использовании Active Record для различных целей. В течение каждого жизненного цикла вызывается определённая последовательность методов, которые вы можете переопределять, чтобы получить возможность тонкой настройки жизненного цикла. Для встраивания своего кода вы также можете отвечать на конкретные события Active Record, которые срабатывают в течение жизненного цикла. Эти события особенно полезны, когда вы разрабатываете поведения, которые требуют тонкой настройки жизненных циклов Active Record.

Ниже мы подробно опишем различные жизненные циклы Active Record и методы/события, которые участвуют в жизненных циклах.

Жизненный цикл создания нового объекта

Когда создаётся новый объект Active Record с помощью оператора new, следующий жизненный цикл имеет место:

  1. Вызывается конструктор класса;
  2. Вызывается [[yii\db\ActiveRecord::init()|init()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]].

Жизненный цикл получения данных

Когда происходит получение данных посредством одного из методов получения данных, каждый вновь создаваемый объект Active Record при заполнении данными проходит следующий жизненный цикл:

  1. Вызывается конструктор класса.
  2. Вызывается [[yii\db\ActiveRecord::init()|init()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]].
  3. Вызывается [[yii\db\ActiveRecord::afterFind()|afterFind()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_AFTER_FIND|EVENT_AFTER_FIND]].

Жизненный цикл сохранения данных

Когда вызывается метод [[yii\db\ActiveRecord::save()|save()]] для вставки или обновления объекта Active Record, следующий жизненный цикл имеет место:

  1. Вызывается [[yii\db\ActiveRecord::beforeValidate()|beforeValidate()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_BEFORE_VALIDATE|EVENT_BEFORE_VALIDATE]]. Если метод возвращает false или свойство события [[yii\base\ModelEvent::isValid]] равно false, оставшиеся шаги не выполняются.
  2. Осуществляется валидация данных. Если валидация закончилась неудачей, после 3-го шага остальные шаги не выполняются.
  3. Вызывается [[yii\db\ActiveRecord::afterValidate()|afterValidate()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_AFTER_VALIDATE|EVENT_AFTER_VALIDATE]].
  4. Вызывается [[yii\db\ActiveRecord::beforeSave()|beforeSave()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_BEFORE_INSERT|EVENT_BEFORE_INSERT]] или событие [[yii\db\ActiveRecord::EVENT_BEFORE_UPDATE|EVENT_BEFORE_UPDATE]]. Если метод возвращает false или свойство события [[yii\base\ModelEvent::isValid]] равно false, оставшиеся шаги не выполняются.
  5. Осуществляется фактическая вставка или обновление данных в базу данных;
  6. Вызывается [[yii\db\ActiveRecord::afterSave()|afterSave()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] или событие [[yii\db\ActiveRecord::EVENT_AFTER_UPDATE|EVENT_AFTER_UPDATE]].

Жизненный цикл удаления данных

Когда вызывается метод [[yii\db\ActiveRecord::delete()|delete()]] для удаления объекта Active Record, следующий жизненный цикл имеет место:

  1. Вызывается [[yii\db\ActiveRecord::beforeDelete()|beforeDelete()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_BEFORE_DELETE|EVENT_BEFORE_DELETE]]. Если метод возвращает false или свойство события [[yii\base\ModelEvent::isValid]] равно false, остальные шаги не выполняются.
  2. Осуществляется фактическое удаление данных из базы данных.
  3. Вызывается [[yii\db\ActiveRecord::afterDelete()|afterDelete()]]: инициируется событие [[yii\db\ActiveRecord::EVENT_AFTER_DELETE|EVENT_AFTER_DELETE]].

Note: Вызов следующих методов НЕ инициирует ни один из вышеприведённых жизненных циклов:

  • [[yii\db\ActiveRecord::updateAll()]]
  • [[yii\db\ActiveRecord::deleteAll()]]
  • [[yii\db\ActiveRecord::updateCounters()]]
  • [[yii\db\ActiveRecord::updateAllCounters()]]

Работа с транзакциями

Есть два способа использования транзакций при работе с Active Record.

Первый способ заключается в том, чтобы явно заключить все вызовы методов Active Record в блок транзакции как показано ниже:

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...другие операции с базой данных...
});

// или по-другому

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...другие операции с базой данных...
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

Note: в коде выше ради совместимости с PHP 5.x и PHP 7.x использованы два блока catch. \Exception реализует интерфейс \Throwable interface начиная с PHP 7.0. Если вы используете только PHP 7 и новее, можете пропустить блок с \Exception.

Второй способ заключается в том, чтобы перечислить операции с базой данных, которые требуют тразнакционного выполнения, в методе [[yii\db\ActiveRecord::transactions()]]. Например:

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // вышеприведённая строка эквивалентна следующей:
            // 'api' => self::OP_ALL,
        ];
    }
}

Метод [[yii\db\ActiveRecord::transactions()]] должен возвращать массив, ключи которого являются именами сценариев, а значения соответствуют операциям, которые должны быть выполнены с помощью транзакций. Вы должны использовать следующие константы для обозначения различных операций базы данных:

  • [[yii\db\ActiveRecord::OP_INSERT|OP_INSERT]]: операция вставки, осуществляемая с помощью метода [[yii\db\ActiveRecord::insert()|insert()]];
  • [[yii\db\ActiveRecord::OP_UPDATE|OP_UPDATE]]: операция обновления, осуществляемая с помощью метода [[yii\db\ActiveRecord::update()|update()]];
  • [[yii\db\ActiveRecord::OP_DELETE|OP_DELETE]]: операция удаления, осуществляемая с помощью метода [[yii\db\ActiveRecord::delete()|delete()]].

Используйте операторы | для объединения вышеприведённых констант при обозначении множества операций. Вы можете также использовать вспомогательную константу [[yii\db\ActiveRecord::OP_ALL|OP_ALL]], чтобы обозначить одной константой все три вышеприведённые операции.

Оптимистическая блокировка

Оптимистическая блокировка - это способ предотвращения конфликтов, которые могут возникать, когда одна и та же строка данных обновляется несколькими пользователями. Например, пользователь A и пользователь B одновременно редактируют одну и ту же wiki-статью. После того, как пользователь A сохранит свои изменения, пользователь B нажимает на кнопку "Сохранить" в попытке также сохранить свои изменения. Т.к. пользователь B работал с фактически-устаревшей версией статьи, было бы неплохо иметь способ предотвратить сохранение его варианта статьи и показать ему некоторое сообщение с подсказкой о том, что произошло.

Оптимистическая блокировка решает вышеприведённую проблему за счёт использования отдельного столбца для сохранения номера версии каждой строки данных. Когда строка данных сохраняется с использованием устаревшего номера версии, выбрасывается исключение [[yii\db\StaleObjectException]], которое предохраняет строку от сохранения. Оптимистическая блокировка поддерживается только тогда, когда вы обновляете или удаляете существующую строку данных, используя методы [[yii\db\ActiveRecord::update()]] или [[yii\db\ActiveRecord::delete()]] соответственно.

Для использования оптимистической блокировки:

  1. Создайте столбец в таблице базы данных, ассоциированной с классом Active Record, для сохранения номера версии каждой строки данных. Столбец должен быть типа big integer (в Mysql это будет BIGINT DEFAULT 0).
  2. Переопределите метод [[yii\db\ActiveRecord::optimisticLock()]] таким образом, чтобы он возвращал название этого столбца.
  3. В веб-форме, которая принимает пользовательский ввод, добавьте скрытое поле для сохранения текущей версии обновляемой строки. Убедитесь, что для вашего атрибута с версией объявлены правила валидации, и валидация проходит успешно.
  4. В действии контроллера, которое занимается обновлением строки данных с использованием Active Record, оберните в блок try...catch код и перехватывайте исключение [[yii\db\StaleObjectException]]. Реализуйте необходимую бизнес-логику (например, возможность слияния изменений, подсказку о том, что данные устарели) для разрешения возникшего конфликта.

Например, предположим, что столбец с версией называется version. Вы можете реализовать оптимистическую блокировку с помощью подобного кода:

// ------ код представления -------

use yii\helpers\Html;

// ...другие поля ввода
echo Html::activeHiddenInput($model, 'version');


// ------ код контроллера -------

use yii\db\StaleObjectException;

public function actionUpdate($id)
{
    $model = $this->findModel($id);

    try {
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    } catch (StaleObjectException $e) {
        // логика разрешения конфликта версий
    }
}

Работа со связными данными

Помимо работы с отдельными таблицами баз данных, Active Record также имеет возможность объединять связные данные, что делает их легко-доступными для получения через основные объекты данных. Например, данные покупателя связаны с данными заказов, потому что один покупатель может осуществить один или несколько заказов. С помощью объявления этой связи вы можете получить возможность доступа к информации о заказе покупателя с помощью выражения $customer->orders, которое возвращает информацию о заказе покупателя в виде массива объектов класса Order, которые являются Active Record объектами.

Объявление связей

Для работы со связными данными посредством Active Record вы прежде всего должны объявить связи в классе Active Record. Эта задача решается простым объявлением методов получения связных данных для каждой интересующей вас связи как показано ниже:

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

В вышеприведённом коде мы объявили связь orders для класса Customer и связь customer для класса Order.

Каждый метод получения связных данных должен быть назван в формате getXyz. Мы называем xyz (первая буква в нижнем регистре) именем связи. Помните, что имена связей чувствительны к регистру.

При объявлении связи, вы должны указать следующую информацию:

  • кратность связи: указывается с помощью вызова метода [[yii\db\ActiveRecord::hasMany()|hasMany()]] или метода [[yii\db\ActiveRecord::hasOne()|hasOne()]]. В вышеприведённом примере вы можете легко увидеть в объявлениях связей, что покупатель может иметь много заказов в то время, как заказ может быть сделан лишь одним покупателем.

  • название связного Active Record класса: указывается в качестве первого параметра для метода [[yii\db\ActiveRecord::hasMany()|hasMany()]] или для метода [[yii\db\ActiveRecord::hasOne()|hasOne()]]. Рекомендуется использовать код Xyz::className(), чтобы получить строку с именем класса, при этом вы сможете воспользоваться возможностями авто-дополнения кода, встроенного в IDE, а также получите обработку ошибок на этапе компиляции.

  • связь между двумя типами данных: указываются столбцы с помощью которых два типа данных связаны. Значения массива - это столбцы основного объекта данных (представлен классом Active Record, в котором объявляется связь), в то время как ключи массива - столбцы связанных данных.

    Есть простой способ запомнить это правило: как вы можете увидеть в примере выше, столбец связной Active Record указывается сразу же после указания самого класса Active Record. Вы видите, что customer_id - это свойство класса Order, а id - свойство класса Customer.

Доступ к связным данным

После объявления связей вы можете получать доступ к связным данным с помощью имён связей. Это происходит таким же образом, каким осуществляется доступ к свойству объекта объявленному с помощью метода получения связных данных. По этой причине, мы называем его свойством связи. Например:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders - это массив объектов Order
$orders = $customer->orders;

Info: когда вы объявляете связь с названием xyz посредством геттера getXyz(), у вас появляется возможность доступа к свойству xyz подобно свойству объекта. Помните, что название связи чувствительно к регистру.

Если связь объявлена с помощью метода [[yii\db\ActiveRecord::hasMany()|hasMany()]], доступ к свойству связи вернёт массив связных объектов Active Record; если связь объявлена с помощью метода [[yii\db\ActiveRecord::hasOne()|hasOne()]], доступ к свойству связи вернёт связный Active Record объект или null, если связные данные не найдены.

Когда вы запрашиваете свойство связи в первый раз, выполняется SQL-выражение как показано в примере выше. Если то же самое свойство запрашивается вновь, будет возвращён результат предыдущего SQL-запроса без повторного выполнения SQL-выражения. Для принудительного повторного выполнения SQL-запроса, вы можете удалить свойство связи с помощью операции: unset($customer->orders).

Note: Несмотря на то, что эта концепция выглядит похожей на концепцию свойств объектов, между ними есть важное различие. Для обычных свойств объектов значения свойств имеют тот же тип, который возвращает геттер. Однако метод получения связных данных возвращает объект [[yii\db\ActiveQuery]], в то время как доступ к свойству связи возвращает объект [[yii\db\ActiveRecord]] или массив таких объектов.

$customer->orders; // массив объектов `Order`
$customer->getOrders(); // объект ActiveQuery

Это полезно при тонкой настройке запросов к связным данным, что будет описано в следующем разделе.

Динамические запросы связных данных

Т.к. метод получения связных данных возвращает объект запроса [[yii\db\ActiveQuery]], вы можете в дальнейшем перед его отправкой в базу данных настроить этот запрос, используя методы построения запросов. Например:

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

В отличие от доступа к данным с помощью свойства связи, каждый раз при выполнении такого динамического запроса посредством метода получения связных данных будет выполняться SQL-запрос, даже если тот же самый динамический запрос был отправлен ранее.

Иногда вы можете даже захотеть настроить объявление связи таким образом, чтобы вы могли более просто осуществлять динамические запросы связных данных. Например, вы можете объявить связь bigOrders как показано ниже:

class Customer extends ActiveRecord
{
    public function getBigOrders($threshold = 100)
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id'])
            ->where('subtotal > :threshold', [':threshold' => $threshold])
            ->orderBy('id');
    }
}

После этого вы сможете выполнять следующие запросы связных данных:

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

Связывание посредством промежуточной таблицы

При проектировании баз данных, когда между двумя таблицами имеется кратность связи many-to-many, обычно вводится промежуточная таблица. Например, таблицы order и item могут быть связаны посредством промежуточной таблицы с названием order_item. Один заказ будет соотносится с несколькими товарами, в то время как один товар будет также соотноситься с несколькими заказами.

При объявлении подобных связей вы можете пользоваться методом [[yii\db\ActiveQuery::via()|via()]] или методом [[yii\db\ActiveQuery::viaTable()|viaTable()]] для указания промежуточной таблицы. Разница между методами [[yii\db\ActiveQuery::via()|via()]] и [[yii\db\ActiveQuery::viaTable()|viaTable()]] заключается в том, что первый метод указывает промежуточную таблицу с помощью названия связи, в то время как второй метод непосредственно указывает промежуточную таблицу. Например:

class Order extends ActiveRecord
{
    public function getItems()
    {
        return $this->hasMany(Item::className(), ['id' => 'item_id'])
            ->viaTable('order_item', ['order_id' => 'id']);
    }
}

или по-другому:

class Order extends ActiveRecord
{
    public function getOrderItems()
    {
        return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
    }

    public function getItems()
    {
        return $this->hasMany(Item::className(), ['id' => 'item_id'])
            ->via('orderItems');
    }
}

Использовать связи, объявленные с помощью промежуточных таблиц, можно точно также, как и обычные связи. Например:

// SELECT * FROM `order` WHERE `id` = 100
$order = Order::findOne(100);

// SELECT * FROM `order_item` WHERE `order_id` = 100
// SELECT * FROM `item` WHERE `item_id` IN (...)
// возвращает массив объектов Item
$items = $order->items;

Отложенная и жадная загрузка

В разделе Доступ к связным данным, мы показывали, что вы можете получать доступ к свойству связи объекта Active Record точно также, как получаете доступ к свойству обычного объекта. SQL-запрос будет выполнен только во время первого доступа к свойству связи. Мы называем подобный способ получения связных данных отложенной загрузкой. Например:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// SQL-запрос не выполняется
$orders2 = $customer->orders;

Отложенная загрузка очень удобна в использовании. Однако этот метод может вызвать проблемы производительности, когда вам понадобится получить доступ к тем же самым свойствам связей для нескольких объектов Active Record. Рассмотрите следующий пример кода. Сколько SQL-запросов будет выполнено?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

Как вы могли заметить по вышеприведённым комментариям кода, будет выполнен 101 SQL-запрос! Это произойдёт из-за того, что каждый раз внутри цикла будет выполняться SQL-запрос при получении доступа к свойству связи orders каждого отдельного объекта Customer.

Для решения этой проблемы производительности вы можете, как показано ниже, использовать подход, который называется жадная загрузка:

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // SQL-запрос не выполняется
    $orders = $customer->orders;
}

Посредством вызова метода [[yii\db\ActiveQuery::with()]], вы указываете объекту Active Record вернуть заказы первых 100 покупателей с помощью одного SQL-запроса. В результате снижаете количество выполняемых SQL-запросов от 101 до 2!

Вы можете жадно загружать одну или несколько связей. Вы можете даже жадно загружать вложенные связи. Вложенная связь - это связь, которая объявлена внутри связного Active Record класса. Например, Customer связан с Order посредством связи orders, а Order связан с Item посредством связи items. При формировании запроса для Customer, вы можете жадно загрузить items, используя нотацию вложенной связи orders.items.

Ниже представлен код, который показывает различные способы использования метода [[yii\db\ActiveQuery::with()|with()]]. Мы полагаем, что класс Customer имеет две связи: orders и country - в то время как класс Order имеет лишь одну связь items.

// жадная загрузка "orders" и "country" одновременно
$customers = Customer::find()->with('orders', 'country')->all();
// аналог с использованием синтаксиса массива
$customers = Customer::find()->with(['orders', 'country'])->all();
// SQL-запрос не выполняется
$orders= $customers[0]->orders;
// SQL-запрос не выполняется
$country = $customers[0]->country;

// жадная загрузка связи "orders" и вложенной связи "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// доступ к деталям первого заказа первого покупателя 
// SQL-запрос не выполняется
$items = $customers[0]->orders[0]->items;

Вы можете жадно загрузить более глубокие вложенные связи, такие как a.b.c.d. Все родительские связи будут жадно загружены. Таким образом, когда вы вызываете метод [[yii\db\ActiveQuery::with()|with()]] с параметром a.b.c.d, вы жадно загрузите связи a, a.b, a.b.c и a.b.c.d.

Info: В целом, когда жадно загружается N связей, среди которых M связей объявлено с помощью промежуточной таблицы, суммарное количество выполняемых SQL-запросов будет равно N+M+1. Заметьте, что вложенная связь a.b.c.d насчитывает 4 связи.

Когда связь жадно загружается, вы можете настроить соответствующий запрос получения связных данных с использованием анонимной функции. Например:

// найти покупателей и получить их вместе с их странами и активными заказами
// SELECT * FROM `customer`
// SELECT * FROM `country` WHERE `id` IN (...)
// SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
$customers = Customer::find()->with([
    'country',
    'orders' => function ($query) {
        $query->andWhere(['status' => Order::STATUS_ACTIVE]);
    },
])->all();

Когда настраивается запрос на получение связных данных для какой-либо связи, вы можете указать название связи в виде ключа массива и использовать анонимную функцию в качестве соответствующего значения этого массива. Анонимная функция получит параметр $query, который представляет собой объект [[yii\db\ActiveQuery]], используемый для выполнения запроса на получение связных данных для данной связи. В вышеприведённом примере кода мы изменили запрос на получение связных данных, наложив на него дополнительное условие выборки статуса заказов.

Note: Если вы вызываете метод [[yii\db\Query::select()|select()]] в процессе жадной загрузки связей, вы должны убедиться, что будут выбраны столбцы, участвующие в объявлении связей. Иначе связные модели будут загружены неправильно. Например:

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer всегда равно null. Для исправления проблемы вы должны сделать следующее:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

Использование JOIN со связями

Note: Материал этого раздела применим только к реляционным базам данных, таким как MySQL, PostgreSQL, и т.д.

Запросы на получение связных данных, которые мы рассмотрели выше, ссылаются только на столбцы основной таблицы при извлечении основной информации. На самом же деле нам часто нужно ссылаться в запросах на столбцы связных таблиц. Например, мы можем захотеть получить покупателей, для которых имеется хотя бы один активный заказ. Для решения этой проблемы мы можем построить запрос с использованием JOIN как показано ниже:

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id`
// WHERE `order`.`status` = 1
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->select('customer.*')
    ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->with('orders')
    ->all();

Note: Важно однозначно указывать в SQL-выражениях имена столбцов при построении запросов на получение связных данных с участием оператора JOIN. Наиболее распространённая практика - предварять названия столбцов с помощью имён соответствующих им таблиц.

Однако лучшим подходом является использование имеющихся объявлений связей с помощью вызова метода [[yii\db\ActiveQuery::joinWith()]]:

$customers = Customer::find()
    ->joinWith('orders')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->all();

Оба подхода выполняют одинаковый набор SQL-запросов. Однако второй подход более прозрачен и прост.

По умолчанию, метод [[yii\db\ActiveQuery::joinWith()|joinWith()]] будет использовать конструкцию LEFT JOIN для объединения основной таблицы со связной. Вы можете указать другой тип операции JOIN (например, RIGHT JOIN) с помощью третьего параметра этого метода - $joinType. Если же вам нужен INNER JOIN, вы можете вместо этого просто вызвать метод [[yii\db\ActiveQuery::innerJoinWith()|innerJoinWith()]].

Вызов метода [[yii\db\ActiveQuery::joinWith()|joinWith()]] будет жадно загружать связные данные по умолчанию. Если вы не хотите получать связные данные, вы можете передать во втором параметре $eagerLoading значение false.

Подобно методу [[yii\db\ActiveQuery::with()|with()]] вы можете объединять данные с одной или несколькими связями; вы можете настроить запрос на получение связных данных "на лету"; вы можете объединять данные с вложенными связями; вы можете смешивать использование метода [[yii\db\ActiveQuery::with()|with()]] и метода [[yii\db\ActiveQuery::joinWith()|joinWith()]]. Например:

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

Иногда во время объединения двух таблиц вам может потребоваться указать некоторые дополнительные условия рядом с оператором ON во время выполнения JOIN-запроса. Это можно сделать с помощью вызова метода [[yii\db\ActiveQuery::onCondition()]] как показано ниже:

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->onCondition(['order.status' => Order::STATUS_ACTIVE]);
    },
])->all();

Вышеприведённый запрос вернёт всех покупателей и для каждого покупателя вернёт все активные заказы. Заметьте, что это поведение отличается от нашего предыдущего примера, в котором возвращались только покупатели, у которых был как минимум один активный заказ.

Info: Когда в объекте [[yii\db\ActiveQuery]] указано условие выборки с помощью метода [[yii\db\ActiveQuery::onCondition()|onCondition()]], это условие будет размещено в конструкции ON, если запрос содержит оператор JOIN. Если же запрос не содержит оператор JOIN, такое условие будет автоматически размещено в конструкции WHERE.

Псевдонимы связанных таблиц

Как уже было отмечено, при использовании в запросе JOIN-ов, приходится явно решать конфликты имён. Поэтому часто таблицам дают псевдонимы. Задать псевдоним для реляционного запроса можно следующим образом:

$query->joinWith([
  'orders' => function ($q) {
      $q->from(['o' => Order::tableName()]);
  },
])

Выглядит это довольно сложно. Либо приходится задавать явно имена таблиц, либо вызывать Order::tableName(). Начиная с версии 2.0.7 вы можете задать и использовать псевдоним для связанной таблицы следующим образом:

// join the orders relation and sort the result by orders.id
$query->joinWith(['orders o'])->orderBy('o.id');

Этот синтаксис работает для простых связей. Если же необходимо использовать связующую таблицу, например $query->joinWith(['orders.product']), то вызовы joinWith вкладываются друг в друга:

$query->joinWith(['orders o' => function($q) {
      $q->joinWith('product p');
  }])
  ->where('o.amount > 100');

Обратные связи

Объявления связей часто взаимны между двумя Active Record классами. Например, Customer связан с Order посредством связи orders, а Order взаимно связан с Customer посредством связи customer.

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

Теперь рассмотрим следующий участок кода:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SELECT * FROM `customer` WHERE `id` = 123
$customer2 = $order->customer;

// выведет "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

Мы думали, что $customer и $customer2 эквивалентны, но оказалось, что нет! Фактически они содержат одинаковые данные, но являются разными объектами. Когда мы получаем доступ к данным посредством $order->customer, выполняется дополнительный SQL-запрос для заполнения нового объекта $customer2.

Чтобы избежать избыточного выполнения последнего SQL-запроса в вышеприведённом примере, мы должны подсказать Yii, что customer - обратная связь относительно orders, и сделаем это с помощью вызова метода [[yii\db\ActiveQuery::inverseOf()|inverseOf()]] как показано ниже:

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
    }
}

Теперь, после этих изменений в объявлении связи, получим:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SQL-запрос не выполняется
$customer2 = $order->customer;

// выведет "same"
echo $customer2 === $customer ? 'same' : 'not the same';

Note: обратные связи не могут быть объявлены для связей, использующих промежуточную таблицу. То есть, если связь объявлена с помощью методов [[yii\db\ActiveQuery::via()|via()]] или [[yii\db\ActiveQuery::viaTable()|viaTable()]], вы не должны вызывать после этого метод [[yii\db\ActiveQuery::inverseOf()|inverseOf()]].

Сохранение связных данных

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

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

// установка атрибута, которой задаёт связь "customer" в объекте Order
$order->customer_id = $customer->id;
$order->save();

Active Record предоставляет метод [[yii\db\ActiveRecord::link()|link()]], который позволяет выполнить эту задачу более красивым способом:

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

$order->link('customer', $customer);

Метод [[yii\db\ActiveRecord::link()|link()]] требует указать название связи и целевой объект Active Record, с которым должна быть установлена связь. Метод изменит значения атрибутов, которые связывают два объекта Active Record, и сохранит их в базу данных. В вышеприведённом примере, метод присвоит атрибуту customer_id объекта Order значение атрибута id объекта Customer и затем сохранит его в базу данных.

Note: Невозможно связать два свежесозданных объекта Active Record.

Преимущество метода [[yii\db\ActiveRecord::link()|link()]] становится ещё более очевидным, когда связь объявлена посредством промежуточной таблицы. Например, вы можете использовать следующий код, чтобы связать объект Order с объектом Item:

$order->link('items', $item);

Вышеприведённый код автоматически вставит строку данных в промежуточную таблицу order_item, чтобы связать объект order с объектом item.

Info: Метод [[yii\db\ActiveRecord::link()|link()]] не осуществляет какую-либо валидацию данных во время сохранения целевого объекта Active Record. На вас лежит ответственность за валидацию любых введённых данных перед вызовом этого метода.

Существует противоположная операция для [[yii\db\ActiveRecord::link()|link()]] - это операция [[yii\db\ActiveRecord::unlink()|unlink()]], она снимает существующую связь с двух объектов Active Record. Например:

$customer = Customer::find()->with('orders')->where(['id' => 123])->one();
$customer->unlink('orders', $customer->orders[0]);

По умолчанию метод [[yii\db\ActiveRecord::unlink()|unlink()]] задаст вторичному ключу (или ключам), который определяет существующую связь, значение null. Однако вы можете запросить удаление строки таблицы, которая содержит значение вторичного ключа, передав значение true в параметре $delete для этого метода.

Если связь построена на основе промежуточной таблицы, вызов метода [[yii\db\ActiveRecord::unlink()|unlink()]] инициирует очистку вторичных ключей в промежуточной таблице, или же удаление соответствующей строки данных в промежуточной таблице, если параметр $delete равен true.

Связывание объектов из разных баз данных

Active Record позволяет вам объявить связи между классами Active Record, которые относятся к разным базам данных. Базы данных могут быть разных типов (например, MySQL и PostgreSQL или MS SQL и MongoDB), и они могут быть запущены на разных серверах. Вы можете использовать тот же самый синтаксис для осуществления запросов выборки связных данных. Например:

// Объект Customer соответствует таблице "customer" в реляционной базе данных (например MySQL)
class Customer extends \yii\db\ActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // у покупателя может быть много комментариев
        return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
    }
}

// Объект Comment соответствует коллекции "comment" в базе данных MongoDB
class Comment extends \yii\mongodb\ActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // комментарий принадлежит одному покупателю
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

$customers = Customer::find()->with('comments')->all();

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

Note: Применимость метода [[yii\db\ActiveQuery::joinWith()|joinWith()]] ограничена базами данных, которые позволяют выполнять запросы между разными базами с использованием оператора JOIN. По этой причине вы не можете использовать этот метод в вышеприведённом примере, т.к. MongoDB не поддерживает операцию JOIN.

Тонкая настройка классов Query

По умолчанию все запросы данных для Active Record поддерживаются с помощью класса [[yii\db\ActiveQuery]]. Для использования собственного класса запроса вам необходимо переопределить метод [[yii\db\ActiveRecord::find()]] и возвращать из него объект вашего собственного класса запроса. Например:

namespace app\models;

use yii\db\ActiveRecord;
use yii\db\ActiveQuery;

class Comment extends ActiveRecord
{
    public static function find()
    {
        return new CommentQuery(get_called_class());
    }
}

class CommentQuery extends ActiveQuery
{
    // ...
}

Теперь, когда вы будете осуществлять получение данных (например, выполните find(), findOne()) или объявите связь (например, hasOne()) с объектом Comment, вы будете работать с объектом класса CommentQuery вместо ActiveQuery.

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

Вы можете настроить класс запроса большим количеством различных способов для улучшения методик построения запросов. Например, можете объявить новые методы построения запросов в собственном классе запросов:

class CommentQuery extends ActiveQuery
{
    public function active($state = true)
    {
        return $this->andWhere(['active' => $state]);
    }
}

Note: Вместо вызова метода [[yii\db\ActiveQuery::where()|where()]] старайтесь во время объявления новых методов построения запросов использовать [[yii\db\ActiveQuery::andWhere()|andWhere()]] или [[yii\db\ActiveQuery::orWhere()|orWhere()]] для добавления дополнительных условий, в этом случае уже заданные условия выборок не будут перезаписаны.

Это позволит вам писать код построения запросов как показано ниже:

$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();

Вы также можете использовать новые методы построения запросов, когда объявляете связи для класса Comment или осуществляете запрос для выборки связных данных:

class Customer extends \yii\db\ActiveRecord
{
    public function getActiveComments()
    {
        return $this->hasMany(Comment::className(), ['customer_id' => 'id'])->active();
    }
}

$customers = Customer::find()->with('activeComments')->all();

// или по-другому:
 
$customers = Customer::find()->with([
    'comments' => function($q) {
        $q->active();
    }
])->all();

Info: В Yii версии 1.1 была концепция с названием scope. Она больше не поддерживается в Yii версии 2.0, и вы можете использовать собственные классы запросов и собственные методы построения запросов, чтобы добиться той же самой цели.

Получение дополнительных атрибутов

Когда объект Active Record заполнен результатами запроса, его атрибуты заполнены значениями соответствующих столбцов из полученного набора данных.

Вы можете получить дополнительные столбцы или значения с помощью запроса и сохранить их внутри объекта Active Record. Например, предположим, что у нас есть таблица 'room', которая содержит информацию о доступных в отеле комнатах. Каждая комната хранит информацию о её геометрических размерах с помощью атрибутов 'length', 'width', 'height'. Представьте, что вам требуется получить список всех доступных комнат, отсортированных по их объёму в порядке убывания. В этом случае вы не можете вычислять объём с помощью PHP, потому что нам требуется сортировать записи по объёму, но вы также хотите отображать объем в списке. Для достижения этой цели, вам необходимо объявить дополнительный атрибут в вашем Active Record классе 'Room', который будет хранить значение 'volume':

class Room extends \yii\db\ActiveRecord
{
    public $volume;

    // ...
}

Далее вам необходимо составить запрос, который вычисляет объём комнаты и выполняет сортировку:

$rooms = Room::find()
    ->select([
        '{{room}}.*', // получить все столбцы
        '([[length]] * [[width]] * [[height]]) AS volume', // вычислить объём
    ])
    ->orderBy('volume DESC') // отсортировать
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // содержит значение, вычисленное с помощью SQL-запроса
}

Возможность выбирать дополнительные атрибуты может быть особенно полезной для агрегирующих запросов. Представьте, что вам необходимо отображать список покупателей с количеством их заказов. Прежде всего вам потребуется объявить класс Customer со связью 'orders' и дополнительным атрибутом для хранения расчётов:

class Customer extends \yii\db\ActiveRecord
{
    public $ordersCount;

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

После этого вы сможете составить запрос, который объединяет заказы и вычисляет их количество:

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // получить все атрибуты покупателя
        'COUNT({{order}}.id) AS ordersCount' // вычислить количество заказов
    ])
    ->joinWith('orders') // обеспечить построение промежуточной таблицы
    ->groupBy('{{customer}}.id') // сгруппировать результаты, чтобы заставить агрегацию работать
    ->all();

Недостаток этого подхода заключается в том, что если данные для поля не загружены по результатам SQL запроса, то они должны быть вычисленны отдельно. Это означает, что запись, полученная посредством обычного запроса без дополнительных полей в разделе 'select', не может вернуть реальное значения для дополнительного поля. Это же касается и только что сохраненной записи.

$room = new Room();
$room->length = 100;
$room->width = 50;
$room->height = 2;

$room->volume; // значение будет равно `null`, т.к. поле не было заполнено

Использование магических методов [[yii\db\BaseActiveRecord::__get()|__get()]] и [[yii\db\BaseActiveRecord::__set()|__set()]] позволяет эмулировать поведение обычного поля:

class Room extends \yii\db\ActiveRecord
{
    private $_volume;

    public function setVolume($volume)
    {
        $this->_volume = (float) $volume;
    }

    public function getVolume()
    {
        if (empty($this->length) || empty($this->width) || empty($this->height)) {
            return null;
        }

        if ($this->_volume === null) {
            $this->setVolume(
                $this->length * $this->width * $this->height
            );
        }

        return $this->_volume;
    }

    // ...
}

Если результат запроса на выборку данных не содержит поле 'volume', то модель сможет расчитать его автоматически используя имеющиеся атрибуты.

Вы также можете вычислять агрегируемые поля используя объявленные отношения:

class Customer extends \yii\db\ActiveRecord
{
    private $_ordersCount;

    public function setOrdersCount($count)
    {
        $this->_ordersCount = (int) $count;
    }

    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // нет смысла выполнять запрос на поиск по пустым ключам
        }

        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // вычисляем агрегацию по требованию из отношения
        }

        return $this->_ordersCount;
    }

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

При такой реализации, в случае когда 'ordersCount' присутствует в разделе 'select' - значение 'Customer::ordersCount' будет заполнено из результатов запроса, в противном случае - оно будет вычислено по первому требованию на основании отношения Customer::orders.

Этот подход также можно использовать для быстрого доступа к некоторым данным отношений, в особенности для агрегации. Например:

class Customer extends \yii\db\ActiveRecord
{
    /**
     * Объявляет виртуальное свойство для агрегируемых данных, доступное только на чтение.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // нет смысла выполнять запрос на поиск по пустым ключам
        }

        return $this->ordersAggregation[0]['counted'];
    }

    /**
     * Объявляет обычное отношение 'orders'.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }

    /**
     * Объявляет новое отношение, основанное на 'orders', которое предоставляет агрегацию.
     */
    public function getOrdersAggregation()
    {
        return $this->getOrders()
            ->select(['customer_id', 'counted' => 'count(*)'])
            ->groupBy('customer_id')
            ->asArray(true);
    }

    // ...
}

foreach (Customer::find()->with('ordersAggregation')->all() as $customer) {
    echo $customer->ordersCount; // выводит агрегируемые данные из отношения без дополнительного запроса благодаря жадной загрузке
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // выводит агрегируемые данные отношения через ленивую загрузку

Заберите ссылку на статью к себе, чтобы потом легко её найти!
Раз уж досюда дочитали, то может может есть желание рассказать об этом месте своим друзьям, знакомым и просто мимо проходящим?
Не надо себя сдерживать! ;)

Старт! Горячий старт на просторы интернета
Старт! Горячий старт на просторы интернета
Старт! Меню