|
|
|
Active Record
|
|
|
|
=============
|
|
|
|
|
|
|
|
[Active Record](http://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()]], чтобы указать с какой именно таблицей связан ваш класс.
|
|
|
|
|
|
|
|
В нижеследующем примере мы объявляем класс 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 являются [моделями](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`.
|
|
|
|
|
|
|
|
> Информация: Т.к. класс [[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,
|
|
|
|
]);
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: Ни метод [[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;
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: Атрибуты объекта 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`,
|
|
|
|
что позволить вам вводить и отображать дни рождения покупателей в формате `'ДД.ММ.ГГГГ'`.
|
|
|
|
|
|
|
|
> Подсказка: Вышеприведённый пример демонстрирует общий способ преобразования данных в различные форматы. Если вы
|
|
|
|
работаете с датами и временем, вы можете использовать [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();
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: В то время как этот способ бережёт память и улучшает производительность, он ближе к низкому слою
|
|
|
|
абстракции базы данных и вы потеряете многие возможности 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
> Подсказка: Вы можете вызвать [[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]] для получения сообщений об ошибках валидации.
|
|
|
|
|
|
|
|
> Подсказка: Если вы уверены, что ваши данные не требуют валидации (например, данные пришли из доверенного источника),
|
|
|
|
вы можете вызвать `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]);
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: Если вы используете метод [[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()]].
|
|
|
|
|
|
|
|
> Примечание: Сравнение старых и новых значений будет осуществлено с помощью оператора `===`, так что значение будет
|
|
|
|
считаться 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="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]);
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: будьте очень осторожны, используя метод [[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]].
|
|
|
|
|
|
|
|
|
|
|
|
> Примечание: Вызов следующих методов НЕ инициирует ни один из вышеприведённых жизненных циклов:
|
|
|
|
> - [[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;
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Второй способ заключается в том, чтобы перечислить операции с базой данных, которые требуют тразнакционного выполнения,
|
|
|
|
в методе [[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::className(), ['customer_id' => 'id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Order extends ActiveRecord
|
|
|
|
{
|
|
|
|
public function getCustomer()
|
|
|
|
{
|
|
|
|
return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
В вышеприведённом коде мы объявили связь `orders` для класса `Customer` и связь `customer` для класса `Order`.
|
|
|
|
|
|
|
|
Каждый метод получения связных данных должен быть назван в формате `getXyz`. Мы называем `xyz` (первая буква в нижнем
|
|
|
|
регистре) *именем связи*. Помните, что имена связей чувствительны к регистру.
|
|
|
|
|
|
|
|
При объявлении связи, вы должны указать следующую информацию:
|
|
|
|
|
|
|
|
- кратность связи: указывается с помощью вызова метода [[yii\db\ActiveRecord::hasMany()|hasMany()]] или метода
|
|
|
|
[[yii\db\ActiveRecord::hasOne()|hasOne()]]. В вышеприведённом примере вы можете легко увидеть в объявлениях связей,
|
|
|
|
что покупатель может иметь много заказов в то время, как заказ может быть сделан лишь одним покупателем.
|
|
|
|
- название связного Active Record класса: указывается в качестве первого параметра для метода
|
|
|
|
[[yii\db\ActiveRecord::hasMany()|hasMany()]] или для метода [[yii\db\ActiveRecord::hasOne()|hasOne()]]. Рекомендуется
|
|
|
|
использовать код `Xyz::className()`, чтобы получить строку с именем класса, при этом вы сможете воспользоваться
|
|
|
|
возможностями авто-дополнения кода, встроенного в IDE, а также получите обработку ошибок на этапе компиляции.
|
|
|
|
- связь между двумя типами данных: указываются столбцы с помощью которых два типа данных связаны. Значения массива - это
|
|
|
|
столбцы основного объекта данных (представлен классом Active Record, в котором объявляется связь), в то время как
|
|
|
|
ключи массива - столбцы связанных данных.
|
|
|
|
|
|
|
|
Есть простой способ запомнить это правило: как вы можете увидеть в примере выше, столбец связной Active Record
|
|
|
|
указывается сразу же после указания самого класса Active Record. Вы видите, что `customer_id` - это свойство класса
|
|
|
|
`Order`, а `id` - свойство класса `Customer`.
|
|
|
|
|
|
|
|
|
|
|
|
### Доступ к связным данным <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;
|
|
|
|
```
|
|
|
|
|
|
|
|
> Информация: когда вы объявляете связь с названием `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)`.
|
|
|
|
|
|
|
|
> Примечание: Несмотря на то, что эта концепция выглядит похожей на концепцию [свойств объектов](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 `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::className(), ['customer_id' => 'id'])
|
|
|
|
->where('subtotal > :threshold', [':threshold' => $threshold])
|
|
|
|
->orderBy('id');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
После этого вы сможете выполнять следующие запросы связных данных:
|
|
|
|
|
|
|
|
```php
|
|
|
|
// SELECT * FROM `order` WHERE `subtotal` > 200 ORDER BY `id`
|
|
|
|
$orders = $customer->getBigOrders(200)->all();
|
|
|
|
|
|
|
|
// SELECT * FROM `order` WHERE `subtotal` > 100 ORDER BY `id`
|
|
|
|
$orders = $customer->bigOrders;
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Связывание посредством промежуточной таблицы <span id="junction-table"></span>
|
|
|
|
|
|
|
|
При проектировании баз данных, когда между двумя таблицами имеется кратность связи many-to-many, обычно вводится
|
|
|
|
[промежуточная таблица](http://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::className(), ['id' => 'item_id'])
|
|
|
|
->viaTable('order_item', ['order_id' => 'id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
или по-другому:
|
|
|
|
|
|
|
|
```php
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Использовать связи, объявленные с помощью промежуточных таблиц, можно точно также, как и обычные связи. Например:
|
|
|
|
|
|
|
|
```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`.
|
|
|
|
|
|
|
|
> Информация: В целом, когда жадно загружается `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]], используемый для выполнения запроса
|
|
|
|
на получение связных данных для данной связи. В вышеприведённом примере кода мы изменили запрос на получение связных
|
|
|
|
данных, наложив на него дополнительное условие выборки статуса заказов.
|
|
|
|
|
|
|
|
> Примечание: Если вы вызываете метод [[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>
|
|
|
|
|
|
|
|
> Примечание: Материал этого раздела применим только к реляционным базам данных, таким как 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();
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: Важно однозначно указывать в 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();
|
|
|
|
```
|
|
|
|
|
|
|
|
Вышеприведённый запрос вернёт *всех* покупателей и для каждого покупателя вернёт все активные заказы. Заметьте, что это
|
|
|
|
поведение отличается от нашего предыдущего примера, в котором возвращались только покупатели, у которых был как минимум
|
|
|
|
один активный заказ.
|
|
|
|
|
|
|
|
> Информация: Когда в объекте [[yii\db\ActiveQuery]] указано условие выборки с помощью метода
|
|
|
|
[[yii\db\ActiveQuery::onCondition()|onCondition()]], это условие будет размещено в конструкции `ON`, если запрос
|
|
|
|
содержит оператор JOIN. Если же запрос не содержит оператор JOIN, такое условие будет автоматически размещено в
|
|
|
|
конструкции `WHERE`.
|
|
|
|
|
|
|
|
|
|
|
|
### Обратные связи <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::className(), ['customer_id' => 'id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Order extends ActiveRecord
|
|
|
|
{
|
|
|
|
public function getCustomer()
|
|
|
|
{
|
|
|
|
return $this->hasOne(Customer::className(), ['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::className(), ['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';
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: обратные связи не могут быть объявлены для связей, использующих [промежуточную таблицу](#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` и затем сохранит его в базу данных.
|
|
|
|
|
|
|
|
> Примечание: Невозможно связать два свежесозданных объекта Active Record.
|
|
|
|
|
|
|
|
Преимущество метода [[yii\db\ActiveRecord::link()|link()]] становится ещё более очевидным, когда связь объявлена
|
|
|
|
посредством [промежуточной таблицы](#junction-table). Например, вы можете использовать следующий код, чтобы связать
|
|
|
|
объект `Order` с объектом `Item`:
|
|
|
|
|
|
|
|
```php
|
|
|
|
$order->link('items', $item);
|
|
|
|
```
|
|
|
|
|
|
|
|
Вышеприведённый код автоматически вставит строку данных в промежуточную таблицу `order_item`, чтобы связать объект
|
|
|
|
`order` с объектом `item`.
|
|
|
|
|
|
|
|
> Информация: Метод [[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')->all();
|
|
|
|
$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::className(), ['customer_id' => 'id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Объект Comment соответствует коллекции "comment" в базе данных MongoDB
|
|
|
|
class Comment extends \yii\mongodb\ActiveRecord
|
|
|
|
{
|
|
|
|
public static function collectionName()
|
|
|
|
{
|
|
|
|
return 'comment';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getCustomer()
|
|
|
|
{
|
|
|
|
// комментарий принадлежит одному покупателю
|
|
|
|
return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$customers = Customer::find()->with('comments')->all();
|
|
|
|
```
|
|
|
|
|
|
|
|
Вы можете использовать большую часть возможностей запросов получения связных данных, которые были описаны в этой главе.
|
|
|
|
|
|
|
|
> Примечание: Применимость метода [[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`.
|
|
|
|
|
|
|
|
> Подсказка: В больших проектах рекомендуется использовать собственные классы запросов, которые будут содержать в себе
|
|
|
|
большую часть кода, связанного с настройкой запросов, таким образом классы Active Record удастся сохранить более
|
|
|
|
чистыми.
|
|
|
|
|
|
|
|
Вы можете настроить класс запроса большим количеством различных способов для улучшения методик построения запросов.
|
|
|
|
Например, можете объявить новые методы построения запросов в собственном классе запросов:
|
|
|
|
|
|
|
|
```php
|
|
|
|
class CommentQuery extends ActiveQuery
|
|
|
|
{
|
|
|
|
public function active($state = true)
|
|
|
|
{
|
|
|
|
return $this->andWhere(['active' => $state]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
> Примечание: Вместо вызова метода [[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::className(), ['customer_id' => 'id'])->active();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$customers = Customer::find()->with('activeComments')->all();
|
|
|
|
|
|
|
|
// или по-другому:
|
|
|
|
|
|
|
|
$customers = Customer::find()->with([
|
|
|
|
'comments' => function($q) {
|
|
|
|
$q->active();
|
|
|
|
}
|
|
|
|
])->all();
|
|
|
|
```
|
|
|
|
|
|
|
|
> Информация: В 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::className(), ['customer_id' => 'id']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
После этого вы сможете составить запрос, который объединяет заказы и вычисляет их количество:
|
|
|
|
|
|
|
|
```php
|
|
|
|
$customers = Customer::find()
|
|
|
|
->select([
|
|
|
|
'{{customer}}.*', // получить все атрибуты покупателя
|
|
|
|
'COUNT({{order}}.id) AS ordersCount' // вычислить количество заказов
|
|
|
|
])
|
|
|
|
->joinWith('orders') // обеспечить построение промежуточной таблицы
|
|
|
|
->groupBy('{{customer}}.id') // сгруппировать результаты, чтобы заставить агрегацию работать
|
|
|
|
->all();
|
|
|
|
```
|