mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-10-31 18:47:33 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			1711 lines
		
	
	
		
			106 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			1711 lines
		
	
	
		
			106 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| Active Record
 | ||
| =============
 | ||
| 
 | ||
| [Active Record](https://ru.wikipedia.org/wiki/ActiveRecord) обеспечивает объектно-ориентированный интерфейс для доступа
 | ||
| и манипулирования данными, хранящимися в базах данных. Класс Active Record соответствует таблице в базе данных, объект
 | ||
| Active Record соответствует строке этой таблицы, а *атрибут* объекта Active Record представляет собой значение
 | ||
| отдельного столбца строки. Вместо непосредственного написания SQL-выражений вы сможете получать доступ к атрибутам
 | ||
| Active Record и вызывать методы Active Record для доступа и манипулирования данными, хранящимися в таблицах базы данных.
 | ||
| 
 | ||
| Для примера предположим, что `Customer` - это класс Active Record, который сопоставлен с таблицей `customer`, а `name` -
 | ||
| столбец в таблице `customer`. Тогда вы можете написать следующий код для вставки новой строки в таблицу `customer`:
 | ||
| 
 | ||
| ```php
 | ||
| $customer = new Customer();
 | ||
| $customer->name = 'Qiang';
 | ||
| $customer->save();
 | ||
| ```
 | ||
| 
 | ||
| Вышеприведённый код аналогичен использованию следующего SQL-выражения в MySQL, которое менее интуитивно, потенциально
 | ||
| может вызвать ошибки и даже проблемы совместимости, если вы используете различные виды баз данных:
 | ||
| 
 | ||
| ```php
 | ||
| $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]] (Имейте в виду, что вследствие
 | ||
|   [бага](http://jira.cubrid.org/browse/APIS-658) в 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 <span id="declaring-ar-classes"></span>
 | ||
| 
 | ||
| Для начала объявите свой собственный класс, унаследовав класс [[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-запросах](db-dao.md#quoting-table-and-column-names).
 | ||
| 
 | ||
| В нижеследующем примере мы объявляем класс Active Record с названием `Customer` для таблицы `customer`.
 | ||
| 
 | ||
| ```php
 | ||
| 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 являются [моделями](structure-models.md). Именно поэтому мы обычно задаём классам Active Record
 | ||
| пространство имён `app\models` (или другое пространство имён, предназначенное для моделей). 
 | ||
| 
 | ||
| Т.к. класс [[yii\db\ActiveRecord]] наследует класс [[yii\base\Model]], он обладает *всеми* возможностями
 | ||
| [моделей](structure-models.md), такими как атрибуты, правила валидации, способы сериализации данных и т.д.
 | ||
| 
 | ||
| 
 | ||
| ## Подключение к базам данных <span id="db-connection"></span>
 | ||
| 
 | ||
| По умолчанию Active Record для доступа и манипулирования данными БД использует
 | ||
| [компонент приложения](structure-application-components.md) `db` в качестве компонента
 | ||
| [[yii\db\Connection|DB connection]]. Как сказано в разделе [Объекты доступа к данным (DAO)](db-dao.md), вы можете
 | ||
| настраивать компонент `db` на уровне конфигурации приложения как показано ниже:
 | ||
| 
 | ||
| ```php
 | ||
| return [
 | ||
|     'components' => [
 | ||
|         'db' => [
 | ||
|             'class' => 'yii\db\Connection',
 | ||
|             'dsn' => 'mysql:host=localhost;dbname=testdb',
 | ||
|             'username' => 'demo',
 | ||
|             'password' => 'demo',
 | ||
|         ],
 | ||
|     ],
 | ||
| ];
 | ||
| ```
 | ||
| Если вы хотите использовать для подключения к базе данных другой компонент подключения, отличный от `db`, вам нужно
 | ||
| переопределить метод [[yii\db\ActiveRecord::getDb()|getDb()]]:
 | ||
| 
 | ||
| ```php
 | ||
| class Customer extends ActiveRecord
 | ||
| {
 | ||
|     // ...
 | ||
| 
 | ||
|     public static function getDb()
 | ||
|     {
 | ||
|         // использовать компонент приложения "db2"
 | ||
|         return \Yii::$app->db2;  
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ## Получение данных <span id="querying-data"></span>
 | ||
| 
 | ||
| После объявления класса Active Record вы можете использовать его для получения данных из соответствующей таблицы базы
 | ||
| данных. Этот процесс, как правило, состоит из следующих трёх шагов:
 | ||
| 
 | ||
| 1. Создать новый объект запроса вызовом метода [[yii\db\ActiveRecord::find()]];
 | ||
| 2. Настроить объект запроса вызовом [методов построения запросов](db-query-builder.md#building-queries);
 | ||
| 3. Вызвать один из [методов получения данных](db-query-builder.md#query-methods) для извлечения данных в виде объектов
 | ||
| Active Record.
 | ||
| 
 | ||
| Как вы могли заметить, эти шаги очень похожи на работу с [построителем запросов](db-query-builder.md). Различие лишь в
 | ||
| том, что для создания объекта запроса вместо оператора `new` используется метод [[yii\db\ActiveRecord::find()]],
 | ||
| возвращающий новый объект запроса, являющийся представителем класса [[yii\db\ActiveQuery]].
 | ||
| 
 | ||
| Ниже приведено несколько примеров использования Active Query для получения данных:
 | ||
| 
 | ||
| ```php
 | ||
| // возвращает покупателя с идентификатором 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 как описано в разделе [Построитель запросов](db-query-builder.md).
 | ||
| 
 | ||
| Т.к. извлечение данных по первичному ключу или значениям отдельных столбцов достаточно распространённая задача, Yii
 | ||
| предоставляет два коротких метода для её решения:
 | ||
| 
 | ||
| - [[yii\db\ActiveRecord::findOne()]]: возвращает один объект Active Record, заполненный первой строкой результата запроса.
 | ||
| - [[yii\db\ActiveRecord::findAll()]]: возвращает массив объектов Active Record, заполненных *всеми* полученными результатами запроса.
 | ||
| 
 | ||
| Оба метода могут принимать параметры в одном из следующих форматов:
 | ||
| 
 | ||
| - скалярное значение: значение интерпретируется как первичный ключ, по которому следует искать. Yii прочитает
 | ||
|   информацию о структуре базы данных и автоматически определит, какой столбец таблицы содержит первичные ключи.
 | ||
| - массив скалярных значений: массив интерпретируется как набор первичных ключей, по которым следует искать.
 | ||
| - ассоциативный массив: ключи массива интерпретируются как названия столбцов, а значения - как содержимое столбцов,
 | ||
|   которое следует искать. За подробностями вы можете обратиться к разделу [Hash Format](db-query-builder.md#hash-format)
 | ||
|   
 | ||
| Нижеследующий код демонстрирует, каким образом эти методы могут быть использованы:
 | ||
| 
 | ||
| ```php
 | ||
| // возвращает покупателя с идентификатором 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: Если вам нужно передать в эти методы данные, полученные от пользователя, убедитесь что передаваемое значение – это скаляр,
 | ||
| > а если необходимо указать условия в формате массива – убедитесь, что пользовательские данные не могут изменить структуру этого массива.
 | ||
| >
 | ||
| > ```php
 | ||
| > // 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()]]:
 | ||
| 
 | ||
| ```php
 | ||
| // возвращает всех неактивных покупателей
 | ||
| $sql = 'SELECT * FROM customer WHERE status=:status';
 | ||
| $customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();
 | ||
| ```
 | ||
| 
 | ||
| Не используйте дополнительные методы построения запросов после вызова метода
 | ||
| [[yii\db\ActiveRecord::findBySql()|findBySql()]], т.к. они будут проигнорированы.
 | ||
| 
 | ||
| 
 | ||
| ## Доступ к данным <span id="accessing-data"></span>
 | ||
| 
 | ||
| Как сказано выше, получаемые из базы данные заполняют объекты Active Record и каждая строка результата запроса
 | ||
| соответствует одному объекту Active Record. Вы можете получить доступ к значениям столбцов с помощью атрибутов этих
 | ||
| объектов. Например так:
 | ||
| 
 | ||
| ```php
 | ||
| // "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).
 | ||
| 
 | ||
| 
 | ||
| ### Преобразование данных <span id="data-transformation"></span>
 | ||
| 
 | ||
| Часто бывает так, что данные вводятся и/или отображаются в формате, который отличается от формата их хранения в базе
 | ||
| данных. Например, в базе данных вы храните дни рождения покупателей в формате UNIX timestamp (что, кстати говоря, не
 | ||
| является хорошим дизайном), в то время как во многих случаях вы хотите манипулировать днями рождения в виде строк
 | ||
| формата `'ДД.ММ.ГГГГ'`. Для достижения этой цели, вы можете объявить методы *преобразования данных* в
 | ||
| ActiveRecord-классе `Customer` как показано ниже:
 | ||
| 
 | ||
| ```php
 | ||
| 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](tutorial-core-validators.md#date) и
 | ||
|   [[yii\jui\DatePicker|DatePicker]], которые проще в использовании и являются более мощными инструментами.
 | ||
| 
 | ||
| 
 | ||
| ### Получение данных в виде массива <span id="data-in-arrays"></span>
 | ||
| 
 | ||
| Несмотря на то, что получение данных в виде Active Record объектов является удобным и гибким, этот способ не всегда
 | ||
| подходит при получении большого количества данных из-за больших накладных расходов памяти. В этом случае вы можете
 | ||
| получить данные в виде PHP-массива, используя перед выполнением запроса метод
 | ||
| [[yii\db\ActiveQuery::asArray()|asArray()]]:
 | ||
| 
 | ||
| ```php
 | ||
| // возвращает всех покупателей
 | ||
| // каждый покупатель будет представлен в виде ассоциативного массива
 | ||
| $customers = Customer::find()
 | ||
|     ->asArray()
 | ||
|     ->all();
 | ||
| ```
 | ||
| 
 | ||
| > Note: В то время как этот способ бережёт память и улучшает производительность, он ближе к низкому слою
 | ||
|   абстракции базы данных и вы потеряете многие возможности Active Record. Важное отличие заключается в типах данных
 | ||
|   значений столбцов. Когда вы получаете данные в виде объектов Active Record, значения столбцов автоматически приводятся
 | ||
|   к типам, соответствующим типам столбцов; с другой стороны, когда вы получаете данные в массивах, значения столбцов
 | ||
|   будут строковыми (до тех пор, пока они являются результатом работы PDO-слоя без какой-либо обработки), несмотря на
 | ||
|   настоящие типы данных соответствующих столбцов.
 | ||
|   
 | ||
| 
 | ||
| ### Пакетное получение данных <span id="data-in-batches"></span>
 | ||
| 
 | ||
| В главе [Построитель запросов](db-query-builder.md) мы объясняли, что вы можете использовать *пакетную выборку* для
 | ||
| снижения расходов памяти при получении большого количества данных из базы. Вы можете использовать такой же подход при
 | ||
| работе с Active Record. Например:
 | ||
| 
 | ||
| ```php
 | ||
| // получить 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
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ## Сохранение данных <span id="inserting-updating-data"></span>
 | ||
| 
 | ||
| Используя Active Record, вы легко можете сохранить данные в базу данных, осуществив следующие шаги:
 | ||
| 
 | ||
| 1. Подготовьте объект Active Record;
 | ||
| 2. Присвойте новые значения атрибутам Active Record;
 | ||
| 3. Вызовите метод [[yii\db\ActiveRecord::save()]] для сохранения данных в базу данных.
 | ||
| 
 | ||
| Например:
 | ||
| 
 | ||
| ```php
 | ||
| // вставить новую строку данных
 | ||
| $customer = new Customer();
 | ||
| $customer->name = 'James';
 | ||
| $customer->email = 'james@example.com';
 | ||
| $customer->save();
 | ||
| 
 | ||
| // обновить имеющуюся строку данных
 | ||
| $customer = Customer::findOne(123);
 | ||
| $customer->email = 'james@newexample.com';
 | ||
| $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()]] как показано ниже:
 | ||
| 
 | ||
| ```php
 | ||
| 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()]]
 | ||
|   непосредственно, чтобы вставить или обновить строку данных в таблице.
 | ||
|   
 | ||
| 
 | ||
| ### Валидация данных <span id="data-validation"></span>
 | ||
| 
 | ||
| Т.к. класс [[yii\db\ActiveRecord]] наследует класс [[yii\base\Model]], он обладает такими же возможностями
 | ||
| [валидации данных](input-validation.md). Вы можете объявить правила валидации переопределив метод
 | ||
| [[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)`, чтобы пропустить валидацию.
 | ||
| 
 | ||
| 
 | ||
| ### Массовое присваивание <span id="massive-assignment"></span>
 | ||
| 
 | ||
| Как и обычные [модели](structure-models.md), объекты Active Record тоже обладают
 | ||
| [возможностью массового присваивания](structure-models.md#massive-assignment). Как будет показано ниже, используя эту
 | ||
| возможность, вы можете одним PHP выражением присвоить значения множества атрибутов Active Record объекту. Запомните
 | ||
| однако, что только [безопасные атрибуты](structure-models.md#safe-attributes) могут быть массово присвоены.
 | ||
| 
 | ||
| ```php
 | ||
| $values = [
 | ||
|     'name' => 'James',
 | ||
|     'email' => 'james@example.com',
 | ||
| ];
 | ||
| 
 | ||
| $customer = new Customer();
 | ||
| 
 | ||
| $customer->attributes = $values;
 | ||
| $customer->save();
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ### Обновление счётчиков <span id="updating-counters"></span>
 | ||
| 
 | ||
| Распространённой задачей является инкремент или декремент столбца в таблице базы данных. Назовём такие столбцы
 | ||
| столбцами-счётчиками. Вы можете использовать метод [[yii\db\ActiveRecord::updateCounters()|updateCounters()]] для
 | ||
| обновления одного или нескольких столбцов-счётчиков. Например:
 | ||
| 
 | ||
| ```php
 | ||
| $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-атрибуты <span id="dirty-attributes"></span>
 | ||
| 
 | ||
| Когда вы вызываете [[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-форм, где каждое значение представлено строкой. Чтобы убедиться в
 | ||
|   корректности типа данных, например для целых значений, вы можете применить
 | ||
|   [фильтрацию данных](input-validation.md#data-filtering): `['attributeName', 'filter', 'filter' => 'intval']`.
 | ||
| 
 | ||
| 
 | ||
| ### Значения атрибутов по умолчанию <span id="default-attribute-values"></span>
 | ||
| 
 | ||
| Некоторые столбцы ваших таблиц могут иметь значения по умолчанию, объявленные в базе данных. Иногда вы можете захотеть
 | ||
| предварительно заполнить этими значениями вашу веб-форму, которая соответствует Active Record объекту. Чтобы избежать
 | ||
| повторного указания этих значений, вы можете вызвать метод
 | ||
| [[yii\db\ActiveRecord::loadDefaultValues()|loadDefaultValues()]] для заполнения соответствующих Active Record атрибутов
 | ||
| значениями по умолчанию, объявленными в базе данных:
 | ||
| 
 | ||
| ```php
 | ||
| $customer = new Customer();
 | ||
| $customer->loadDefaultValues();
 | ||
| // $customer->xyz получит значение по умолчанию, которое было указано при объявлении столбца "xyz"
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ### Приведение типов атрибутов <span id="attributes-typecasting"></span>
 | ||
| 
 | ||
| При заполнении результатами запроса [[yii\db\ActiveRecord]] производит автоматическое приведение типов для значений
 | ||
| атрибутов на основе информации из [схемы базы данны](db-dao.md#database-schema). Это позволяет данным, полученным из
 | ||
| колонки таблицы объявленной как целое, заноситься в экземпляр 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](db-query-builder.md).
 | ||
| 
 | ||
| #### Массивы в PostgreSQL
 | ||
| После заполнения данных значение из столбца `Array` будет автоматически декодировано из нотации PgSQL в объект [[yii\db\ArrayExpression|ArrayExpression]]. Он реализует интерфейс PHP `ArrayAccess`, так что вы можете использовать его в качестве массива, или вызвать `->getValue ()`, чтобы получить сам массив.
 | ||
| 
 | ||
| Чтобы сохранить значение атрибута в столбец массива, ActiveRecord автоматически создаст объект [[yii\db\Array Expression|ArrayExpression]], который будет закодирован [QueryBuilder](db-query-builder.md) в строковое представление массива PgSQL.
 | ||
| 
 | ||
| Можно также использовать условия для столбцов JSON:
 | ||
| 
 | ||
| ```php
 | ||
| $query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])
 | ||
| ```
 | ||
| Дополнительные сведения о системе построения выражений см. [Query Builder – добавление пользовательских условий и выражений](db-query-builder.md#adding-custom-conditions-and-expressions)
 | ||
| 
 | ||
| ### Обновление нескольких строк данных <span id="updating-multiple-rows"></span>
 | ||
| 
 | ||
| Методы, представленные выше, работают с отдельными Active Record объектами, инициируя вставку или обновление данных для
 | ||
| отдельной строки таблицы. Вместо них для обновления нескольких строк одновременно можно использовать метод
 | ||
| [[yii\db\ActiveRecord::updateAll()|updateAll()]], который является статическим.
 | ||
| 
 | ||
| ```php
 | ||
| // 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()]] для
 | ||
| обновления значений столбцов-счётчиков в нескольких строках одновременно.
 | ||
| 
 | ||
| ```php
 | ||
| // UPDATE `customer` SET `age` = `age` + 1
 | ||
| Customer::updateAllCounters(['age' => 1]);
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ## Удаление данных <span id="deleting-data"></span>
 | ||
| 
 | ||
| Для удаления одной отдельной строки данных сначала получите Active Record объект, соответствующий этой строке, а затем
 | ||
| вызовите метод [[yii\db\ActiveRecord::delete()]].
 | ||
| 
 | ||
| ```php
 | ||
| $customer = Customer::findOne(123);
 | ||
| $customer->delete();
 | ||
| ```
 | ||
| 
 | ||
| Вы можете вызвать [[yii\db\ActiveRecord::deleteAll()]] для удаления всех или нескольких строк данных одновременно.
 | ||
| Например:
 | ||
| 
 | ||
| ```php
 | ||
| Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);
 | ||
| ```
 | ||
| 
 | ||
| > Note: будьте очень осторожны, используя метод [[yii\db\ActiveRecord::deleteAll()|deleteAll()]], потому что он
 | ||
|   может полностью удалить все данные из вашей таблицы, если вы сделаете ошибку при указании условий удаления.
 | ||
| 
 | ||
| 
 | ||
| ## Жизненные циклы Active Record <span id="ar-life-cycles"></span>
 | ||
| 
 | ||
| Важно понимать как устроены жизненные циклы Active Record при использовании Active Record для различных целей.
 | ||
| В течение каждого жизненного цикла вызывается определённая последовательность методов, которые вы можете переопределять,
 | ||
| чтобы получить возможность тонкой настройки жизненного цикла. Для встраивания своего кода вы также можете отвечать на
 | ||
| конкретные события Active Record, которые срабатывают в течение жизненного цикла. Эти события особенно полезны, когда
 | ||
| вы разрабатываете [поведения](concept-behaviors.md), которые требуют тонкой настройки жизненных циклов Active Record.
 | ||
| 
 | ||
| Ниже мы подробно опишем различные жизненные циклы Active Record и методы/события, которые участвуют в жизненных циклах.
 | ||
| 
 | ||
| 
 | ||
| ### Жизненный цикл создания нового объекта <span id="new-instance-life-cycle"></span>
 | ||
| 
 | ||
| Когда создаётся новый объект Active Record с помощью оператора `new`, следующий жизненный цикл имеет место:
 | ||
| 
 | ||
| 1. Вызывается конструктор класса;
 | ||
| 2. Вызывается [[yii\db\ActiveRecord::init()|init()]]:
 | ||
|    инициируется событие [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]].
 | ||
| 
 | ||
| 
 | ||
| ### Жизненный цикл получения данных <span id="querying-data-life-cycle"></span>
 | ||
| 
 | ||
| Когда происходит получение данных посредством одного из [методов получения данных](#querying-data), каждый вновь
 | ||
| создаваемый объект 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]].
 | ||
| 
 | ||
| 
 | ||
| ### Жизненный цикл сохранения данных <span id="saving-data-life-cycle"></span>
 | ||
| 
 | ||
| Когда вызывается метод [[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]].
 | ||
|    
 | ||
| 
 | ||
| ### Жизненный цикл удаления данных <span id="deleting-data-life-cycle"></span>
 | ||
| 
 | ||
| Когда вызывается метод [[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()]] 
 | ||
| 
 | ||
| 
 | ||
| ## Работа с транзакциями <span id="transactional-operations"></span>
 | ||
| 
 | ||
| Есть два способа использования [транзакций](db-dao.md#performing-transactions) при работе с Active Record.
 | ||
| 
 | ||
| Первый способ заключается в том, чтобы явно заключить все вызовы методов Active Record в блок транзакции как показано
 | ||
| ниже:
 | ||
| 
 | ||
| ```php
 | ||
| $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](https://www.php.net/manual/ru/class.throwable.php)
 | ||
| > начиная с PHP 7.0. Если вы используете только PHP 7 и новее, можете пропустить блок с `\Exception`.
 | ||
| 
 | ||
| Второй способ заключается в том, чтобы перечислить операции с базой данных, которые требуют тразнакционного выполнения,
 | ||
| в методе [[yii\db\ActiveRecord::transactions()]]. Например:
 | ||
| 
 | ||
| ```php
 | ||
| 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()]] должен возвращать массив, ключи которого являются именами 
 | ||
| [сценариев](structure-models.md#scenarios), а значения соответствуют операциям, которые должны быть выполнены с помощью
 | ||
| транзакций. Вы должны использовать следующие константы для обозначения различных операций базы данных:
 | ||
| 
 | ||
| * [[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]], чтобы обозначить одной константой все три
 | ||
| вышеприведённые операции.
 | ||
| 
 | ||
| 
 | ||
| ## Оптимистическая блокировка <span id="optimistic-locks"></span>
 | ||
| 
 | ||
| Оптимистическая блокировка - это способ предотвращения конфликтов, которые могут возникать, когда одна и та же строка
 | ||
| данных обновляется несколькими пользователями. Например, пользователь 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`. Вы можете реализовать оптимистическую блокировку с 
 | ||
| помощью подобного кода:
 | ||
| 
 | ||
| ```php
 | ||
| // ------ код представления -------
 | ||
| 
 | ||
| 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) {
 | ||
|         // логика разрешения конфликта версий
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ## Работа со связными данными <span id="relational-data"></span>
 | ||
| 
 | ||
| Помимо работы с отдельными таблицами баз данных, Active Record также имеет возможность объединять связные данные, что
 | ||
| делает их легко-доступными для получения через основные объекты данных. Например, данные покупателя связаны с данными
 | ||
| заказов, потому что один покупатель может осуществить один или несколько заказов. С помощью объявления этой связи вы
 | ||
| можете получить возможность доступа к информации о заказе покупателя с помощью выражения `$customer->orders`, которое
 | ||
| возвращает информацию о заказе покупателя в виде массива объектов класса `Order`, которые являются Active Record
 | ||
| объектами.
 | ||
| 
 | ||
| 
 | ||
| ### Объявление связей <span id="declaring-relations"></span>
 | ||
| 
 | ||
| Для работы со связными данными посредством Active Record вы прежде всего должны объявить связи в классе Active Record.
 | ||
| Эта задача решается простым объявлением *методов получения связных данных* для каждой интересующей вас связи как
 | ||
| показано ниже:
 | ||
| 
 | ||
| ```php
 | ||
| class Customer extends ActiveRecord
 | ||
| {
 | ||
|     public function getOrders()
 | ||
|     {
 | ||
|         return $this->hasMany(Order::class, ['customer_id' => 'id']);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| class Order extends ActiveRecord
 | ||
| {
 | ||
|     public function getCustomer()
 | ||
|     {
 | ||
|         return $this->hasOne(Customer::class, ['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::class`, чтобы получить строку с именем класса, при этом вы сможете воспользоваться
 | ||
|   возможностями авто-дополнения кода, встроенного в IDE, а также получите обработку ошибок на этапе компиляции.
 | ||
| - связь между двумя типами данных: указываются столбцы с помощью которых два типа данных связаны. Значения массива - это
 | ||
|   столбцы  основного объекта данных (представлен классом Active Record, в котором объявляется связь), в то время как
 | ||
|   ключи массива - столбцы связанных данных.
 | ||
| 
 | ||
|   Есть простой способ запомнить это правило: как вы можете увидеть в примере выше, столбец связной Active Record
 | ||
|   указывается сразу после указания самого класса Active Record. Вы видите, что `customer_id` - это свойство класса
 | ||
|   `Order`, а `id` - свойство класса `Customer`.
 | ||
|   
 | ||
| > Warning: Имя связи `relation` зарезервировано. Его использование приведёт к ошибке `ArgumentCountError`.
 | ||
| 
 | ||
| ### Доступ к связным данным <span id="accessing-relational-data"></span>
 | ||
| 
 | ||
| После объявления связей вы можете получать доступ к связным данным с помощью имён связей. Это происходит таким же
 | ||
| образом, каким осуществляется доступ к [свойству](concept-properties.md) объекта объявленному с помощью метода получения
 | ||
| связных данных. По этой причине, мы называем его *свойством связи*. Например:
 | ||
| 
 | ||
| ```php
 | ||
| // 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` подобно [свойству объекта](concept-properties.md). Помните, что название связи чувствительно
 | ||
|   к регистру.
 | ||
|   
 | ||
| Если связь объявлена с помощью метода [[yii\db\ActiveRecord::hasMany()|hasMany()]], доступ к свойству связи вернёт
 | ||
| массив связных объектов Active Record; если связь объявлена с помощью метода [[yii\db\ActiveRecord::hasOne()|hasOne()]],
 | ||
| доступ к свойству связи вернёт связный Active Record объект или `null`, если связные данные не найдены.
 | ||
| 
 | ||
| Когда вы запрашиваете свойство связи в первый раз, выполняется SQL-выражение как показано в примере выше. Если то же
 | ||
| самое свойство запрашивается вновь, будет возвращён результат предыдущего SQL-запроса без повторного выполнения
 | ||
| SQL-выражения. Для принудительного повторного выполнения SQL-запроса, вы можете удалить свойство связи с помощью
 | ||
| операции: `unset($customer->orders)`.
 | ||
| 
 | ||
| > Note: Несмотря на то, что эта концепция выглядит похожей на концепцию [свойств объектов](concept-properties.md),
 | ||
| > между ними есть важное различие. Для обычных свойств объектов значения свойств имеют тот же тип, который возвращает
 | ||
| > геттер. Однако метод получения связных данных возвращает объект [[yii\db\ActiveQuery]], в то время как доступ к
 | ||
| > свойству связи возвращает объект [[yii\db\ActiveRecord]] или массив таких объектов.
 | ||
| > ```php
 | ||
| > $customer->orders; // массив объектов `Order`
 | ||
| > $customer->getOrders(); // объект ActiveQuery
 | ||
| > ```
 | ||
| > Это полезно при тонкой настройке запросов к связным данным, что будет описано в следующем разделе.
 | ||
| 
 | ||
| 
 | ||
| ### Динамические запросы связных данных <span id="dynamic-relational-query"></span>
 | ||
| 
 | ||
| Т.к. метод получения связных данных возвращает объект запроса [[yii\db\ActiveQuery]], вы можете в дальнейшем перед его
 | ||
| отправкой в базу данных настроить этот запрос, используя методы построения запросов. Например:
 | ||
| 
 | ||
| ```php
 | ||
| $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` как показано ниже: 
 | ||
| 
 | ||
| ```php
 | ||
| class Customer extends ActiveRecord
 | ||
| {
 | ||
|     public function getBigOrders($threshold = 100)
 | ||
|     {
 | ||
|         return $this->hasMany(Order::class, ['customer_id' => 'id'])
 | ||
|             ->where('subtotal > :threshold', [':threshold' => $threshold])
 | ||
|             ->orderBy('id');
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| После этого вы сможете выполнять следующие запросы связных данных:
 | ||
| 
 | ||
| ```php
 | ||
| // 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;
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ### Связывание посредством промежуточной таблицы <span id="junction-table"></span>
 | ||
| 
 | ||
| При проектировании баз данных, когда между двумя таблицами имеется кратность связи many-to-many, обычно вводится 
 | ||
| [промежуточная таблица](https://en.wikipedia.org/wiki/Junction_table). Например, таблицы `order` и `item` могут быть
 | ||
| связаны посредством промежуточной таблицы с названием `order_item`. Один заказ будет соотноситься с несколькими товарами,
 | ||
| в то время как один товар будет также соотноситься с несколькими заказами.
 | ||
| 
 | ||
| При объявлении подобных связей вы можете пользоваться методом [[yii\db\ActiveQuery::via()|via()]] или методом
 | ||
| [[yii\db\ActiveQuery::viaTable()|viaTable()]] для указания промежуточной таблицы. Разница между методами
 | ||
| [[yii\db\ActiveQuery::via()|via()]] и [[yii\db\ActiveQuery::viaTable()|viaTable()]] заключается в том, что первый
 | ||
| метод указывает промежуточную таблицу с помощью названия связи, в то время как второй метод непосредственно указывает
 | ||
| промежуточную таблицу. Например:
 | ||
| 
 | ||
| ```php
 | ||
| class Order extends ActiveRecord
 | ||
| {
 | ||
|     public function getItems()
 | ||
|     {
 | ||
|         return $this->hasMany(Item::class, ['id' => 'item_id'])
 | ||
|             ->viaTable('order_item', ['order_id' => 'id']);
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| или по-другому:
 | ||
| 
 | ||
| ```php
 | ||
| class Order extends ActiveRecord
 | ||
| {
 | ||
|     public function getOrderItems()
 | ||
|     {
 | ||
|         return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
 | ||
|     }
 | ||
| 
 | ||
|     public function getItems()
 | ||
|     {
 | ||
|         return $this->hasMany(Item::class, ['id' => 'item_id'])
 | ||
|             ->via('orderItems');
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| Использовать связи, объявленные с помощью промежуточных таблиц, можно точно также, как и обычные связи. Например:
 | ||
| 
 | ||
| ```php
 | ||
| // 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;
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ### Отложенная и жадная загрузка <span id="lazy-eager-loading"></span>
 | ||
| 
 | ||
| В разделе [Доступ к связным данным](#accessing-relational-data), мы показывали, что вы можете получать доступ к свойству
 | ||
| связи объекта Active Record точно также, как получаете доступ к свойству обычного объекта. SQL-запрос будет выполнен
 | ||
| только во время первого доступа к свойству связи. Мы называем подобный способ получения связных данных *отложенной
 | ||
| загрузкой*. Например:
 | ||
| 
 | ||
| ```php
 | ||
| // 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-запросов будет выполнено?
 | ||
| 
 | ||
| ```php
 | ||
| // 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`.
 | ||
| 
 | ||
| Для решения этой проблемы производительности вы можете, как показано ниже, использовать подход, который называется
 | ||
| *жадная загрузка*:
 | ||
| 
 | ||
| ```php
 | ||
| // 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`.
 | ||
| 
 | ||
| ```php
 | ||
| // жадная загрузка "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` связей объявлено с помощью
 | ||
|   [промежуточной таблицы](#junction-table), суммарное количество выполняемых SQL-запросов будет равно `N+M+1`. Заметьте,
 | ||
|   что вложенная связь `a.b.c.d` насчитывает 4 связи.
 | ||
| 
 | ||
| Когда связь жадно загружается, вы можете настроить соответствующий запрос получения связных данных с использованием
 | ||
| анонимной функции. Например:
 | ||
| 
 | ||
| ```php
 | ||
| // найти покупателей и получить их вместе с их странами и активными заказами
 | ||
| // 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()]] в процессе жадной загрузки связей, вы должны
 | ||
| > убедиться, что будут выбраны столбцы, участвующие в объявлении связей. Иначе связные модели будут загружены
 | ||
| > неправильно. Например:
 | ||
| >
 | ||
| > ```php
 | ||
| > $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 со связями <span id="joining-with-relations"></span>
 | ||
| 
 | ||
| > Note: Материал этого раздела применим только к реляционным базам данных, таким как MySQL, PostgreSQL, и т.д.
 | ||
| 
 | ||
| Запросы на получение связных данных, которые мы рассмотрели выше, ссылаются только на столбцы основной таблицы при
 | ||
| извлечении основной информации. На самом же деле нам часто нужно ссылаться в запросах на столбцы связных таблиц.
 | ||
| Например, мы можем захотеть получить покупателей, для которых имеется хотя бы один активный заказ. Для решения этой
 | ||
| проблемы мы можем построить запрос с использованием JOIN как показано ниже:
 | ||
| 
 | ||
| ```php
 | ||
| // 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()]]:
 | ||
| 
 | ||
| ```php
 | ||
| $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()]] будет [жадно загружать](#lazy-eager-loading) связные данные
 | ||
| по умолчанию. Если вы не хотите получать связные данные, вы можете передать во втором параметре `$eagerLoading` значение
 | ||
| `false`. 
 | ||
| 
 | ||
| Подобно методу [[yii\db\ActiveQuery::with()|with()]] вы можете объединять данные с одной или несколькими связями; вы 
 | ||
| можете настроить запрос на получение связных данных "на лету"; вы можете объединять данные с вложенными связями; вы
 | ||
| можете смешивать использование метода [[yii\db\ActiveQuery::with()|with()]] и метода 
 | ||
| [[yii\db\ActiveQuery::joinWith()|joinWith()]]. Например:
 | ||
| 
 | ||
| ```php
 | ||
| $customers = Customer::find()->joinWith([
 | ||
|     'orders' => function ($query) {
 | ||
|         $query->andWhere(['>', 'subtotal', 100]);
 | ||
|     },
 | ||
| ])->with('country')
 | ||
|     ->all();
 | ||
| ```
 | ||
| 
 | ||
| Иногда во время объединения двух таблиц вам может потребоваться указать некоторые дополнительные условия рядом с
 | ||
| оператором `ON` во время выполнения JOIN-запроса. Это можно сделать с помощью вызова метода 
 | ||
| [[yii\db\ActiveQuery::onCondition()]] как показано ниже:
 | ||
| 
 | ||
| ```php
 | ||
| // 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`.
 | ||
|   
 | ||
| #### Псевдонимы связанных таблиц <span id="relation-table-aliases"></span>
 | ||
| 
 | ||
| Как уже было отмечено, при использовании в запросе JOIN-ов, приходится явно решать конфликты имён. Поэтому часто таблицам
 | ||
| дают псевдонимы. Задать псевдоним для реляционного запроса можно следующим образом:
 | ||
| 
 | ||
| ```php
 | ||
| $query->joinWith([
 | ||
|   'orders' => function ($q) {
 | ||
|       $q->from(['o' => Order::tableName()]);
 | ||
|   },
 | ||
| ])
 | ||
| ```
 | ||
| 
 | ||
| Выглядит это довольно сложно. Либо приходится задавать явно имена таблиц, либо вызывать `Order::tableName()`.
 | ||
| Начиная с версии 2.0.7 вы можете задать и использовать псевдоним для связанной таблицы следующим образом:
 | ||
| 
 | ||
| ```php
 | ||
| // join the orders relation and sort the result by orders.id
 | ||
| $query->joinWith(['orders o'])->orderBy('o.id');
 | ||
| ```
 | ||
| 
 | ||
| Этот синтаксис работает для простых связей. Если необходимо использовать связующую таблицу, например 
 | ||
| `$query->joinWith(['orders.product'])`, то вызовы joinWith вкладываются друг в друга:
 | ||
| 
 | ||
| ```php
 | ||
| $query->joinWith(['orders o' => function($q) {
 | ||
|       $q->joinWith('product p');
 | ||
|   }])
 | ||
|   ->where('o.amount > 100');
 | ||
| ```
 | ||
| 
 | ||
| 
 | ||
| ### Обратные связи <span id="inverse-relations"></span>
 | ||
| 
 | ||
| Объявления связей часто взаимны между двумя Active Record классами. Например, `Customer` связан с `Order` посредством
 | ||
| связи `orders`, а `Order` взаимно связан с `Customer` посредством связи `customer`.
 | ||
| 
 | ||
| ```php
 | ||
| class Customer extends ActiveRecord
 | ||
| {
 | ||
|     public function getOrders()
 | ||
|     {
 | ||
|         return $this->hasMany(Order::class, ['customer_id' => 'id']);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| class Order extends ActiveRecord
 | ||
| {
 | ||
|     public function getCustomer()
 | ||
|     {
 | ||
|         return $this->hasOne(Customer::class, ['id' => 'customer_id']);
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| Теперь рассмотрим следующий участок кода:
 | ||
| 
 | ||
| ```php
 | ||
| // 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()]] как показано ниже:
 | ||
| 
 | ||
| ```php
 | ||
| class Customer extends ActiveRecord
 | ||
| {
 | ||
|     public function getOrders()
 | ||
|     {
 | ||
|         return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| Теперь, после этих изменений в объявлении связи, получим:
 | ||
| 
 | ||
| ```php
 | ||
| // 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: обратные связи не могут быть объявлены для связей, использующих [промежуточную таблицу](#junction-table).
 | ||
|   То есть, если связь объявлена с помощью методов [[yii\db\ActiveQuery::via()|via()]] или
 | ||
|   [[yii\db\ActiveQuery::viaTable()|viaTable()]], вы не должны вызывать после этого метод
 | ||
|   [[yii\db\ActiveQuery::inverseOf()|inverseOf()]].
 | ||
| 
 | ||
| 
 | ||
| ## Сохранение связных данных <span id="saving-relations"></span>
 | ||
| 
 | ||
| Во время работы со связными данными вам часто требуется установить связи между двумя разными видами данных или удалить
 | ||
| существующие связи. Это требует установки правильных значений для столбцов, с помощью которых заданы связи. При
 | ||
| использовании Active Record вам может понадобится завершить участок кода следующим образом:
 | ||
| 
 | ||
| ```php
 | ||
| $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()]], который позволяет выполнить эту задачу
 | ||
| более красивым способом:
 | ||
| 
 | ||
| ```php
 | ||
| $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()]] становится ещё более очевидным, когда связь объявлена
 | ||
| посредством [промежуточной таблицы](#junction-table). Например, вы можете использовать следующий код, чтобы связать
 | ||
| объект `Order` с объектом `Item`:
 | ||
| 
 | ||
| ```php
 | ||
| $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. Например:
 | ||
| 
 | ||
| ```php
 | ||
| $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`.
 | ||
| 
 | ||
| 
 | ||
| ## Связывание объектов из разных баз данных <span id="cross-database-relations"></span> 
 | ||
| 
 | ||
| Active Record позволяет вам объявить связи между классами Active Record, которые относятся к разным базам данных. Базы
 | ||
| данных могут быть разных типов (например, MySQL и PostgreSQL или MS SQL и MongoDB), и они могут быть запущены на разных
 | ||
| серверах. Вы можете использовать тот же самый синтаксис для осуществления запросов выборки связных данных. Например:
 | ||
| 
 | ||
| ```php
 | ||
| // Объект Customer соответствует таблице "customer" в реляционной базе данных (например MySQL)
 | ||
| class Customer extends \yii\db\ActiveRecord
 | ||
| {
 | ||
|     public static function tableName()
 | ||
|     {
 | ||
|         return 'customer';
 | ||
|     }
 | ||
| 
 | ||
|     public function getComments()
 | ||
|     {
 | ||
|         // у покупателя может быть много комментариев
 | ||
|         return $this->hasMany(Comment::class, ['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::class, ['id' => 'customer_id']);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| $customers = Customer::find()->with('comments')->all();
 | ||
| ```
 | ||
| 
 | ||
| Вы можете использовать большую часть возможностей запросов получения связных данных, которые были описаны в этой главе.
 | ||
| 
 | ||
| > Note: Применимость метода [[yii\db\ActiveQuery::joinWith()|joinWith()]] ограничена базами данных, которые
 | ||
|   позволяют выполнять запросы между разными базами с использованием оператора JOIN. По этой причине вы не можете
 | ||
|   использовать этот метод в вышеприведённом примере, т.к. MongoDB не поддерживает операцию JOIN.
 | ||
| 
 | ||
| 
 | ||
| ## Тонкая настройка классов Query <span id="customizing-query-classes"></span>
 | ||
| 
 | ||
| По умолчанию все запросы данных для Active Record поддерживаются с помощью класса [[yii\db\ActiveQuery]]. Для
 | ||
| использования собственного класса запроса вам необходимо переопределить метод [[yii\db\ActiveRecord::find()]] и
 | ||
| возвращать из него объект вашего собственного класса запроса. Например:
 | ||
|  
 | ||
| ```php
 | ||
| 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 удастся сохранить более
 | ||
|   чистыми.
 | ||
|   
 | ||
| Вы можете настроить класс запроса большим количеством различных способов для улучшения методик построения запросов.
 | ||
| Например, можете объявить новые методы построения запросов в собственном классе запросов:
 | ||
| 
 | ||
| ```php
 | ||
| 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()]] для добавления дополнительных условий, в этом случае уже заданные условия
 | ||
|   выборок не будут перезаписаны.
 | ||
| 
 | ||
| Это позволит вам писать код построения запросов как показано ниже:
 | ||
|  
 | ||
| ```php
 | ||
| $comments = Comment::find()->active()->all();
 | ||
| $inactiveComments = Comment::find()->active(false)->all();
 | ||
| ```
 | ||
| 
 | ||
| Вы также можете использовать новые методы построения запросов, когда объявляете связи для класса `Comment` или
 | ||
| осуществляете запрос для выборки связных данных:
 | ||
| 
 | ||
| ```php
 | ||
| class Customer extends \yii\db\ActiveRecord
 | ||
| {
 | ||
|     public function getActiveComments()
 | ||
|     {
 | ||
|         return $this->hasMany(Comment::class, ['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':
 | ||
| 
 | ||
| ```php
 | ||
| class Room extends \yii\db\ActiveRecord
 | ||
| {
 | ||
|     public $volume;
 | ||
| 
 | ||
|     // ...
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| Далее вам необходимо составить запрос, который вычисляет объём комнаты и выполняет сортировку:
 | ||
| 
 | ||
| ```php
 | ||
| $rooms = Room::find()
 | ||
|     ->select([
 | ||
|         '{{room}}.*', // получить все столбцы
 | ||
|         '([[length]] * [[width]] * [[height]]) AS volume', // вычислить объём
 | ||
|     ])
 | ||
|     ->orderBy('volume DESC') // отсортировать
 | ||
|     ->all();
 | ||
| 
 | ||
| foreach ($rooms as $room) {
 | ||
|     echo $room->volume; // содержит значение, вычисленное с помощью SQL-запроса
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| Возможность выбирать дополнительные атрибуты может быть особенно полезной для агрегирующих запросов. Представьте, что
 | ||
| вам необходимо отображать список покупателей с количеством их заказов. Прежде всего вам потребуется объявить класс 
 | ||
| `Customer` со связью 'orders' и дополнительным атрибутом для хранения расчётов:
 | ||
| 
 | ||
| ```php
 | ||
| class Customer extends \yii\db\ActiveRecord
 | ||
| {
 | ||
|     public $ordersCount;
 | ||
| 
 | ||
|     // ...
 | ||
| 
 | ||
|     public function getOrders()
 | ||
|     {
 | ||
|         return $this->hasMany(Order::class, ['customer_id' => 'id']);
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| После этого вы сможете составить запрос, который объединяет заказы и вычисляет их количество:
 | ||
| 
 | ||
| ```php
 | ||
| $customers = Customer::find()
 | ||
|     ->select([
 | ||
|         '{{customer}}.*', // получить все атрибуты покупателя
 | ||
|         'COUNT({{order}}.id) AS ordersCount' // вычислить количество заказов
 | ||
|     ])
 | ||
|     ->joinWith('orders') // обеспечить построение промежуточной таблицы
 | ||
|     ->groupBy('{{customer}}.id') // сгруппировать результаты, чтобы заставить агрегацию работать
 | ||
|     ->all();
 | ||
| ```
 | ||
| 
 | ||
| Недостаток этого подхода заключается в том, что если данные для поля не загружены по результатам SQL запроса, то они
 | ||
| должны быть вычисленны отдельно. Это означает, что запись, полученная посредством обычного запроса без дополнительных полей в
 | ||
| разделе 'select', не может вернуть реальное значения для дополнительного поля. Это же касается и только что сохранненой
 | ||
| записи.
 | ||
| 
 | ||
| ```php
 | ||
| $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()]]
 | ||
| позволяет эмулировать поведение обычного поля:
 | ||
| 
 | ||
| ```php
 | ||
| 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', то модель сможет расчитать его автоматически
 | ||
| используя имеющиеся атрибуты.
 | ||
| 
 | ||
| Вы также можете вычислять агрегируемые поля используя объявленные отношения:
 | ||
| 
 | ||
| ```php
 | ||
| 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::class, ['customer_id' => 'id']);
 | ||
|     }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| При такой реализации, в случае когда 'ordersCount' присутсвует в разделе 'select' - значение 'Customer::ordersCount' будет
 | ||
| заполнено из результатов запроса, в противном случае - оно будет вычислено по первому требованию на основании отношения `Customer::orders`.
 | ||
| 
 | ||
| Этот подход также можно использовать для быстрого доступа к некоторым данным отношений, в особенности для агрегации.
 | ||
| Например:
 | ||
| 
 | ||
| ```php
 | ||
| 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::class, ['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; // выводит агрегируемые данные отношения через ленивую загрузку
 | ||
| ```
 | 
