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

Например, предположим, что Customer является классом Active Record, который связан с таблицей клиента, а 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: через yii\db\ActiveRecord
  • PostgreSQL: через yii\db\ActiveRecord
  • SQLite: через yii\db\ActiveRecord
  • Microsoft SQL Server: через yii\db\ActiveRecord
  • Oracle: через yii\db\ActiveRecord
  • CUBRID: через yii\db\ActiveRecord
  • Sphinx: через yii\sphinx\ActiveRecord, требуется расширение yii2-sphinx
  • ElasticSearch: через yii\elasticsearch\ActiveRecord, требуется расширение yii2-elasticsearch

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

  • Edis: через yii\redis\ActiveRecord, требуется расширение yii2-redis
  • MongoDB: через yii\mongodb\ActiveRecord, требуется расширение yii2-mongodb

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

Для начала объявите класс Active Record, расширив yii\db\ActiveRecord.

Установка имени таблицы

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

Также можно применить табличное значение по умолчанию. Например, если tablePrefix - tbl_, Клиент становится 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 the name of the table associated with this ActiveRecord class.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Активные записи называются "моделями"

Экземпляры Active Record рассматриваются как модели. По этой причине мы обычно помещаем классы Active Record в пространство имен app\models (или другие пространства имен для хранения классов модели).

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

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

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

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

Если вы хотите использовать другое подключение к базе данных, отличное от компонента db, вам следует переопределить метод getDb():

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // use the "db2" application component
        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 для запроса данных:

// return a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// return all active customers and order them by their IDs
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// return the number of active customers
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// return all customers in an array indexed by customer IDs
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

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

Поскольку обычная задача запроса по значениям первичного ключа или набора значений столбца, Yii предоставляет для этой цели два метода сокращения:

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

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

  • Скалярное значение: значение рассматривается как искомое значение первичного ключа, которое нужно искать. Yii автоматически определит, какой столбец является столбцом первичного ключа, прочитав информацию схемы базы данных.
  • Массив скалярных значений: массив рассматривается как искомые значения первичного ключа, которые нужно искать.
  • Ассоциативный массив: ключи - это имена столбцов, а значения - соответствующие искомые значения столбцов, которые нужно искать. Пожалуйста, обратитесь к Хэш-формату для получения более подробной информации.

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

// returns a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// returns customers whose ID is 100, 101, 123 or 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// returns an active customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// returns all inactive customers
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

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

// returns all inactive customers
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

Не вызывайте дополнительные методы построения запроса после вызова метода findBySql(), поскольку они будут игнорироваться.

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

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

// "id" and "email" are the names of columns in the "customer" table
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

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

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

Часто случается, что вводимые и / или отображаемые данные имеют формат, отличный от того, который используется при хранении данных в базе данных. Например, в базе данных вы храните дни рождения клиентов в качестве временных меток UNIX (хотя это не очень хороший дизайн), в то время как в большинстве случаев вы хотели бы манипулировать днями рождения как строки в формате 'YYYY/MM/DD'. Для достижения этой цели вы можете определить методы преобразования данных в классе Active Record, например:

class Customer extends ActiveRecord
{
    // ...

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

Теперь в вашем PHP-коде вместо доступа к $customer->birthday вы получите доступ к $customer->birthdayText, который позволит вам вводить и отображать дни рождения клиента в формате 'YYYY/MM/DD'.

Извлечение данных в массивах

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

// return all customers
// each customer is returned as an associative array
$customers = Customer::find()
    ->asArray()
    ->all();

Извлечение данных в пакетах

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

// fetch 10 customers at a time
foreach (Customer::find()->batch(10) as $customers) {
    // $customers is an array of 10 or fewer Customer objects
}

// fetch 10 customers at a time and iterate them one by one
foreach (Customer::find()->each(10) as $customer) {
    // $customer is a Customer object
}

// batch query with eager loading
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer is a Customer object with the 'orders' relation populated
}

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

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

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

Например:

// insert a new row of data
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// update an existing row of data
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

Метод save() может либо вставлять, либо обновлять строку данных, в зависимости от состояния экземпляра Active Record. Если экземпляр вновь создан с помощью нового оператора, вызов save() приведет к вставке новой строки; Если экземпляр является результатом метода запроса, вызов save() обновит строку, связанную с экземпляром.

Вы можете различать два состояния экземпляра Active Record, проверяя его значение свойства isNewRecord. Это свойство также используется функцией save() изнутри следующим образом:

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

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

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

Когда вы вызываете save(), по умолчанию он будет вызывать validate() автоматически. Только когда пройдет проверка, будет ли она фактически сохранена; Иначе он просто вернет false, и вы можете проверить свойство errors для получения сообщений об ошибках проверки.

Массовое назначение

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

$values = [
    'name' => 'James',
    'email' => 'james@example.com',
];

$customer = new Customer();

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

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

Общей задачей является увеличение или уменьшение столбца в таблице базы данных. Мы называем эти столбцы «встречными столбцами». Вы можете использовать updateCounters() для обновления одного или нескольких столбцов счетчика. Например:

$post = Post::findOne(100);

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

Загрязненные атрибуты

Когда вы вызываете save() для сохранения экземпляра Active Record, сохраняются только грязные атрибуты. Атрибут считается грязным, если его значение было изменено, поскольку оно было загружено из БД или сохранено в БД совсем недавно. Обратите внимание, что проверка данных будет выполняться независимо от того, имеет ли экземпляр Active Record грязные атрибуты или нет.

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

Если вас интересуют значения атрибутов до их последней модификации, вы можете вызвать getOldAttributes() или getOldAttribute().

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

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

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz will be assigned the default value declared when defining the "xyz" column

Атрибуты Typecasting

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

  • Значения float не преобразуются и представляются в виде строк, в противном случае они могут потерять точность.
  • Преобразование целочисленных значений зависит от целочисленной емкости используемой операционной системы. В частности: значения столбца, объявленного как «unsigned integer» или «big integer», будут преобразованы в целое число только в 64-битную операционную систему, а в 32-битных - они будут представлены в виде строк.

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

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

Описанные выше методы работают с отдельными экземплярами Active Record, вызывая вставку или обновление отдельных строк таблицы. Чтобы обновить несколько строк одновременно, вы должны вместо этого вызвать метод updateAll(), который является статическим методом.

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

Точно так же вы можете вызвать 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]);

Циклы активной записи

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

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

Жизненный цикл нового экземпляра

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

  1. Конструктор класса.
  2. Init(): инициирует событие EVENT_INIT.

Запрос жизненного цикла данных

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

  1. Конструктор класса.
  2. Init(): инициирует событие EVENT_INIT.
  3. afterFind(): инициирует событие EVENT_AFTER_FIND.

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

При вызове функции save() для вставки или обновления экземпляра Active Record произойдет следующий жизненный цикл:

  1. Событие EVENT_BEFORE_VALIDATE. Если метод возвращает false или yii\base\ModelEvent::$isValid имеет значение false, остальные шаги будут пропущены.
  2. Выполняет проверку данных. Если проверка данных завершилась неудачно, шаги после шага 3 будут пропущены.
  3. Событие EVENT_AFTER_VALIDATE.
  4. EVENT_BEFORE_INSERT или EVENT_BEFORE_UPDATE. Если метод возвращает false или yii\base\ModelEvent::$isValid имеет значение false, остальные шаги будут пропущены.
  5. Выполняет фактическую вставку или обновление данных.
  6. EVENT_AFTER_INSERT или EVENT_AFTER_UPDATE.

Удаление жизненного цикла данных

При вызове delete() для удаления экземпляра Active Record произойдет следующий жизненный цикл:

  1. Событие EVENT_BEFORE_DELETE. Если метод возвращает false или yii\base\ModelEvent::$isValid имеет значение false, остальные шаги будут пропущены.
  2. Выполняет фактическое удаление данных.
  3. Событие EVENT_AFTER_DELETE.

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

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

Обновление жизненного цикла данных

При вызове refresh() для обновления экземпляра Active Record событие EVENT_AFTER_REFRESH запускается, если обновление завершено успешно, а метод возвращает значение true.

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

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

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
});

// or alternatively

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

Второй способ - перечислить операции БД, требующие поддержки транзакций в методе 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,
            // the above is equivalent to the following:
            // 'api' => self::OP_ALL,
        ];
    }
}

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

  • OP_INSERT: операция вставки, выполняемая insert();
  • OP_UPDATE: операция обновления, выполняемая update ();
  • OP_DELETE: операция удаления, выполняемая функцией delete ().

Используйте | операторы для объединения указанных констант для указания нескольких операций. Вы также можете использовать сокращенную константу OP_ALL для ссылки на все три операции выше.

Транзакции, созданные с помощью этого метода, будут запущены до вызова beforeSave() и будут зафиксированы после запуска afterSave().

Оптимистические замки

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

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

Чтобы использовать оптимистическую блокировку:

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

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

// ------ view code -------

use yii\helpers\Html;

// ...other input fields
echo Html::activeHiddenInput($model, 'version');


// ------ controller code -------

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) {
        // logic to resolve the conflict
    }
}

Работа с реляционными данными

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

Объявление отношений

Для работы с реляционными данными с использованием 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']);
    }
}

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

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

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

  • Кратность отношения: определяется путем вызова hasMany() или hasOne(). В приведенном выше примере вы можете легко прочитать в объявлениях отношений, что у клиента много заказов, а в заказе есть только один клиент.
  • Имя связанного класса Active Record: указанный в качестве первого параметра для hasMany() или hasOne(). Рекомендуемой практикой является вызов Xyz::className(), чтобы получить строку имени класса, чтобы вы могли получать поддержку автозавершения IDE, а также обнаружение ошибок на этапе компиляции.
  • Связь между двумя типами данных: специфицирует столбец (столбцы), через который связаны два типа данных. Значения массива - это столбцы первичных данных (представленные классом 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 is an array of Order objects
$orders = $customer->orders;

Если отношение объявлено с помощью hasMany(), обращение к этому отношению будет возвращать массив связанных экземпляров Active Record; Если отношение объявлено с hasOne(), обращение к свойству отношения вернет соответствующий экземпляр Active Record или null, если соответствующие данные не найдены.

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

Динамический реляционный запрос

Поскольку метод отношения возвращает экземпляр 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;

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

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

При объявлении таких отношений вы должны вызвать либо via(), либо через Table(), чтобы указать таблицу соединений. Разница между via() и 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 (...)
// returns an array of Item objects
$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;

// no SQL executed
$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-инструкция! Это происходит потому, что каждый раз, когда вы обращаетесь к свойству отношений orders другого объекта Customer в цикле for, выполняется инструкция SQL.

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

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

foreach ($customers as $customer) {
    // no SQL executed
    $orders = $customer->orders;
}

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

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

Следующий код показывает различное использование with(). Мы предполагаем, что класс Customer имеет два порядка orders и country, в то время как класс Order имеет один элемент items.

// eager loading both "orders" and "country"
$customers = Customer::find()->with('orders', 'country')->all();
// equivalent to the array syntax below
$customers = Customer::find()->with(['orders', 'country'])->all();
// no SQL executed 
$orders= $customers[0]->orders;
// no SQL executed 
$country = $customers[0]->country;

// eager loading "orders" and the nested relation "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// access the items of the first order of the first customer
// no SQL executed
$items = $customers[0]->orders[0]->items;

Вы можете с нетерпением загружать глубоко вложенные отношения, такие как a.b.c.d. Все родительские отношения будут загружены. То есть, когда вы вызываете с помощью () с помощью a.b.c.d, вы будете с нетерпением загружать a, a, b, a.b.c и a.b.c.d.

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

// find customers and bring back together their country and active orders
// 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, используемый для выполнения реляционного запроса для отношения. В приведенном выше примере кода мы модифицируем реляционный запрос, добавив дополнительное условие о статусе заказа.

Присоединение к отношениям

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

// 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();

Однако лучше использовать существующие декларации отношений, вызывая yii\db\ActiveQuery::joinWith():

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

Оба подхода выполняют один и тот же набор операторов SQL. Последний подход намного чище и суше.

По умолчанию joinWith () будет использовать LEFT JOIN для присоединения к основной таблице со связанной таблицей. Вы можете указать другой тип соединения (например, RIGHT JOIN) через его третий параметр $joinType. Если тип соединения, который вы хотите, - INNER JOIN, вы можете просто вызвать innerJoinWith().

Вызов joinWith() будет с готовностью загружать связанные данные по умолчанию. Если вы не хотите вводить связанные данные, вы можете указать его второй параметр $eagerLoading как false.

Как и в случае with(), вы можете присоединиться к одному или нескольким отношениям; Вы можете настраивать запросы отношения на лету; Вы можете присоединиться к вложенным отношениям; И вы можете смешивать использование with() и 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();

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

Алиасы таблицы связей

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

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

Это, однако, выглядит очень сложным и включает либо жесткое кодирование имени таблицы связанных объектов, либо вызов Order::tableName(). Вы можете определить и использовать псевдоним для таблицы соотношений, например:

// 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;

// displays "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

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

Чтобы избежать избыточного выполнения последней инструкции SQL в приведенном выше примере, мы должны сказать Yii, что customer является обратной связью orders, вызывая метод 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];

// No SQL will be executed
$customer2 = $order->customer;

// displays "same"
echo $customer2 === $customer ? 'same' : 'not the same';

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

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

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

// setting the attribute that defines the "customer" relation in Order
$order->customer_id = $customer->id;
$order->save();

Active Record предоставляет метод link(), который позволяет вам более эффективно выполнять эту задачу:

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

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

Метод link() требует указать имя отношения и целевой экземпляр Active Record, с которым следует установить связь. Этот метод изменит значения атрибутов, которые связывают два экземпляра Active Record и сохраняют их в базе данных. В приведенном выше примере атрибут customer_id экземпляра Order будет установлен как значение атрибута id экземпляра Customer и затем сохранен в базе данных.

Преимущество использования link() еще более очевидно, когда отношение определяется через таблицу соединений. Например, вы можете использовать следующий код, чтобы связать экземпляр Order с экземпляром Item:

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

Приведенный выше код будет автоматически вставлять строку в таблицу соединений order_item, чтобы связать заказ с элементом.

Противоположная операция для link() - unlink(), которая нарушает существующее отношение между двумя экземплярами Active Record. Например:

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

По умолчанию метод unlink() устанавливает значения внешнего ключа, которые определяют существующее отношение, равным null. Однако вы можете удалить строку таблицы, содержащую значение внешнего ключа, передав параметр $delete как true для метода.

Когда в связи имеется связующая таблица, вызов unlink() приведет к удалению внешних ключей в таблице соединений или к удалению соответствующей строки в таблице соединений, если $delete равно true.

Отношения между базами данных

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

// Customer is associated with the "customer" table in a relational database (e.g. MySQL)
class Customer extends \yii\db\ActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // a customer has many comments
        return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
    }
}

// Comment is associated with the "comment" collection in a MongoDB database
class Comment extends \yii\mongodb\ActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // a comment has one customer
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

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

Настройка классов запросов

По умолчанию все запросы Active Record поддерживаются yii\db\ActiveQuery. Чтобы использовать настраиваемый класс запросов в классе Active Record, вы должны переопределить метод yii\db\ActiveRecord::find() и возвратить экземпляр своего настроенного класса запроса. Например:

// file Comment.php
namespace app\models;

use yii\db\ActiveRecord;

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

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

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

// file CommentQuery.php
namespace app\models;

use yii\db\ActiveQuery;

class CommentQuery extends ActiveQuery
{
    // conditions appended by default (can be skipped)
    public function init()
    {
        $this->andOnCondition(['deleted' => false]);
        parent::init();
    }

    // ... add customized query methods here ...

    public function active($state = true)
    {
        return $this->andOnCondition(['active' => $state]);
    }
}

Это позволяет писать код построения запроса следующим образом:

$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()->joinWith('activeComments')->all();

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

$customers = Customer::find()->joinWith([
    'comments' => function($q) {
        $q->active();
    }
])->all();

Выбор дополнительных полей

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

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

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

    // ...
}

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

$rooms = Room::find()
    ->select([
        '{{room}}.*', // select all columns
        '([[length]] * [[width]] * [[height]]) AS volume', // calculate a volume
    ])
    ->orderBy('volume DESC') // apply sort
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // contains value calculated by 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}}.*', // select all customer fields
        'COUNT({{order}}.id) AS ordersCount' // calculate orders count
    ])
    ->joinWith('orders') // ensure table junction
    ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works
    ->all();

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

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

$room->volume; // this value will be `null`, since it was not declared yet

Используя магические методы __get () и __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;
    }

    // ...
}

Когда запрос select не предоставляет том, модель сможет автоматически его вычислить, используя атрибуты модели. Вы также можете рассчитать поля агрегации, используя определенные отношения:

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

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

    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }

        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // calculate aggregation on demand from relation
        }

        return $this->_ordersCount;
    }

    // ...

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

С помощью этого кода в случае, если в выражении «select» присутствует «ordersCount» - Customer::ordersCount будет заполняться результатами запроса, в противном случае он будет рассчитываться по требованию, используя отношение Customer::orders. Этот подход может также использоваться для создания ярлыков для некоторых реляционных данных, особенно для агрегирования. Например:

class Customer extends \yii\db\ActiveRecord
{
    /**
     * Defines read-only virtual property for aggregation data.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }
        
        return empty($this->ordersAggregation) ? 0 : $this->ordersAggregation[0]['counted'];
    }

    /**
     * Declares normal 'orders' relation.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }

    /**
     * Declares new relation based on 'orders', which provides aggregation.
     */
    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; // outputs aggregation data from relation without extra query due to eager loading
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // output aggregation data from lazy loaded relation