# Active Record
> 注意:該章節還在開發中。
[Active Record](http://zh.wikipedia.org/wiki/Active_Record)?(活動記錄,以下簡稱AR)提供了一個面向對象的接口, 用以訪問數據庫中的數據。一個 AR 類關聯一張數據表, 每個 AR 對象對應表中的一行,對象的屬性(即 AR 的特性Attribute)映射到數據行的對應列。 一條活動記錄(AR對象)對應數據表的一行,AR對象的屬性則映射該行的相應列。 您可以直接以面向對象的方式來操縱數據表中的數據,媽媽再不用擔心我需要寫原生 SQL 語句啦。
例如,假定?`Customer`?AR 類關聯著?`customer`?表,且該類的?`name`?屬性代表?`customer`?表的?`name`?列。 你可以寫以下代碼來哉`customer`?表里插入一行新的記錄:
用 AR 而不是原生的 SQL 語句去執行數據庫查詢,可以調用直觀方法來實現相同目標。如,調用 yii\db\ActiveRecord::save() 方法將執行插入或更新輪詢,將在該 AR 類關聯的數據表新建或更新一行數據:
~~~
$customer = new Customer();
$customer->name = 'Qiang';
$customer->save(); // 一行新數據插入 customer 表
~~~
上面的代碼和使用下面的原生 SQL 語句是等效的,但顯然前者更直觀, 更不易出錯,并且面對不同的數據庫系統(DBMS, Database Management System)時更不容易產生兼容性問題。
~~~
$db->createCommand('INSERT INTO customer (name) VALUES (:name)', [
':name' => 'Qiang',
])->execute();
~~~
下面是所有目前被 Yii 的 AR 功能所支持的數據庫列表:
* MySQL 4.1 及以上:通過 yii\db\ActiveRecord
* PostgreSQL 7.3 及以上:通過 yii\db\ActiveRecord
* SQLite 2 和 3:通過 yii\db\ActiveRecord
* Microsoft SQL Server 2010 及以上:通過 yii\db\ActiveRecord
* Oracle: 通過 yii\db\ActiveRecord
* CUBRID 9.1 及以上:通過 yii\db\ActiveRecord
* Sphinx:通過 yii\sphinx\ActiveRecord,需求?`yii2-sphinx`?擴展
* ElasticSearch:通過 yii\elasticsearch\ActiveRecord,需求?`yii2-elasticsearch`?擴展
* Redis 2.6.12 及以上:通過 yii\redis\ActiveRecord,需求?`yii2-redis`?擴展
* MongoDB 1.3.0 及以上:通過 yii\mongodb\ActiveRecord,需求?`yii2-mongodb`?擴展
如你所見,Yii 不僅提供了對關系型數據庫的 AR 支持,還提供了 NoSQL 數據庫的支持。 在這個教程中,我們會主要描述對關系型數據庫的 AR 用法。 然而,絕大多數的內容在 NoSQL 的 AR 里同樣適用。
## 聲明 AR 類
要想聲明一個 AR 類,你需要擴展 yii\db\ActiveRecord 基類, 并實現?`tableName`?方法,返回與之相關聯的的數據表的名稱:
~~~
namespace app\models;
use yii\db\ActiveRecord;
class Customer extends ActiveRecord
{
/**
* @return string 返回該AR類關聯的數據表名
*/
public static function tableName()
{
return 'customer';
}
}
~~~
## 訪問列數據
AR 把相應數據行的每一個字段映射為 AR 對象的一個個特性變量(Attribute) 一個特性就好像一個普通對象的公共屬性一樣(public property)。 特性變量的名稱和對應字段的名稱是一樣的,且大小姓名。
使用以下語法讀取列的值:
~~~
// "id" 和 "mail" 是 $customer 對象所關聯的數據表的對應字段名
$id = $customer->id;
$email = $customer->email;
~~~
要改變列值,只要給關聯屬性賦新值并保存對象即可:
~~~
$customer->email = 'james@example.com';
$customer->save();
~~~
## 建立數據庫連接
AR 用一個 yii\db\Connection 對象與數據庫交換數據。 默認的,它使用?`db`?組件作為其連接對象。詳見[數據庫基礎](http://www.yiichina.com/doc/guide/2.0/database-basics)章節, 你可以在應用程序配置文件中設置下?`db`?組件,就像這樣,
~~~
return [
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=testdb',
'username' => 'demo',
'password' => 'demo',
],
],
];
~~~
如果在你的應用中應用了不止一個數據庫,且你需要給你的 AR 類使用不同的數據庫鏈接(DB connection) ,你可以覆蓋掉 yii\db\ActiveRecord::getDb() 方法:
~~~
class Customer extends ActiveRecord
{
// ...
public static function getDb()
{
return \Yii::$app->db2; // 使用名為 "db2" 的應用組件
}
}
~~~
## 查詢數據
AR 提供了兩種方法來構建 DB 查詢并向 AR 實例里填充數據:
* yii\db\ActiveRecord::find()
* yii\db\ActiveRecord::findBySql()
以上兩個方法都會返回 yii\db\ActiveQuery 實例,該類繼承自yii\db\Query, 因此,他們都支持同一套靈活且強大的 DB 查詢方法,如`where()`,`join()`,`orderBy()`,等等。 下面的這些案例展示了一些可能的玩法:
~~~
// 取回所有活躍客戶(狀態為 *active* 的客戶)并以他們的 ID 排序:
$customers = Customer::find()
->where(['status' => Customer::STATUS_ACTIVE])
->orderBy('id')
->all();
// 返回ID為1的客戶:
$customer = Customer::find()
->where(['id' => 1])
->one();
// 取回活躍客戶的數量:
$count = Customer::find()
->where(['status' => Customer::STATUS_ACTIVE])
->count();
// 以客戶ID索引結果集:
$customers = Customer::find()->indexBy('id')->all();
// $customers 數組以 ID 為索引
// 用原生 SQL 語句檢索客戶:
$sql = 'SELECT * FROM customer';
$customers = Customer::findBySql($sql)->all();
~~~
> 小技巧:在上面的代碼中,`Customer::STATUS_ACTIVE`?是一個在?`Customer`?類里定義的常量。(譯注:這種常量的值一般都是tinyint)相較于直接在代碼中寫死字符串或數字,使用一個更有意義的常量名稱是一種更好的編程習慣。
有兩個快捷方法:`findOne`?和?`findAll()`?用來返回一個或者一組`ActiveRecord`實例。前者返回第一個匹配到的實例,后者返回所有。 例如:
~~~
// 返回 id 為 1 的客戶
$customer = Customer::findOne(1);
// 返回 id 為 1 且狀態為 *active* 的客戶
$customer = Customer::findOne([
'id' => 1,
'status' => Customer::STATUS_ACTIVE,
]);
// 返回id為1、2、3的一組客戶
$customers = Customer::findAll([1, 2, 3]);
// 返回所有狀態為 "deleted" 的客戶
$customer = Customer::findAll([
'status' => Customer::STATUS_DELETED,
]);
~~~
### 以數組形式獲取數據
有時候,我們需要處理很大量的數據,這時可能需要用一個數組來存儲取到的數據, 從而節省內存。你可以用?`asArray()`?函數做到這一點:
~~~
// 以數組而不是對象形式取回客戶信息:
$customers = Customer::find()
->asArray()
->all();
// $customers 的每個元素都是鍵值對數組
~~~
### 批量獲取數據
在?[Query Builder(查詢構造器)](http://www.yiichina.com/doc/guide/2.0/query-builder)?里,我們已經解釋了當需要從數據庫中查詢大量數據時,你可以用?*batch query(批量查詢)*來限制內存的占用。 你可能也想在 AR 里使用相同的技巧,比如這樣……
~~~
// 一次提取 10 個客戶信息
foreach (Customer::find()->batch(10) as $customers) {
// $customers 是 10 個或更少的客戶對象的數組
}
// 一次提取 10 個客戶并一個一個地遍歷處理
foreach (Customer::find()->each(10) as $customer) {
// $customer 是一個 ”Customer“ 對象
}
// 貪婪加載模式的批處理查詢
foreach (Customer::find()->with('orders')->each() as $customer) {
}
~~~
## 操作數據
AR 提供以下方法插入、更新和刪除與 AR 對象關聯的那張表中的某一行:
* yii\db\ActiveRecord::save()
* yii\db\ActiveRecord::insert()
* yii\db\ActiveRecord::update()
* yii\db\ActiveRecord::delete()
AR 同時提供了一下靜態方法,可以應用在與某 AR 類所關聯的整張表上。 用這些方法的時候千萬要小心,因為他們作用于整張表! 比如,`deleteAll()`?會刪除掉表里**所有**的記錄。
* yii\db\ActiveRecord::updateCounters()
* yii\db\ActiveRecord::updateAll()
* yii\db\ActiveRecord::updateAllCounters()
* yii\db\ActiveRecord::deleteAll()
下面的這些例子里,詳細展現了如何使用這些方法:
~~~
// 插入新客戶的記錄
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save(); // 等同于 $customer->insert();
// 更新現有客戶記錄
$customer = Customer::findOne($id);
$customer->email = 'james@example.com';
$customer->save(); // 等同于 $customer->update();
// 刪除已有客戶記錄
$customer = Customer::findOne($id);
$customer->delete();
// 刪除多個年齡大于20,性別為男(Male)的客戶記錄
Customer::deleteAll('age > :age AND gender = :gender', [':age' => 20, ':gender' => 'M']);
// 所有客戶的age(年齡)字段加1:
Customer::updateAllCounters(['age' => 1]);
~~~
> 須知:`save()`?方法會調用?`insert()`?和?`update()`?中的一個, 用哪個取決于當前 AR 對象是不是新對象(在函數內部,他會檢查 yii\db\ActiveRecord::isNewRecord 的值)。 若 AR 對象是由?`new`?操作符 初始化出來的,`save()`?方法會在表里*插入*一條數據; 如果一個 AR 是由?`find()`?方法獲取來的, 則?`save()`?會*更新*表里的對應行記錄。
### 數據輸入與有效性驗證
由于AR繼承自yii\base\Model,所以它同樣也支持[Model](http://www.yiichina.com/doc/guide/2.0/model)的數據輸入、驗證等特性。例如,你可以聲明一個rules方法用來覆蓋掉yii\base\Model::rules()里的;你也可以給AR實例批量賦值;你也可以通過調用yii\base\Model::validate()執行數據驗證。
當你調用?`save()`、`insert()`、`update()`?這三個方法時,會自動調用yii\base\Model::validate()方法。如果驗證失敗,數據將不會保存進數據庫。
下面的例子演示了如何使用AR 獲取/驗證用戶輸入的數據并將他們保存進數據庫:
~~~
// 新建一條記錄
$model = new Customer;
if ($model->load(Yii::$app->request->post()) && $model->save()) {
// 獲取用戶輸入的數據,驗證并保存
}
// 更新主鍵為$id的AR
$model = Customer::findOne($id);
if ($model === null) {
throw new NotFoundHttpException;
}
if ($model->load(Yii::$app->request->post()) && $model->save()) {
// 獲取用戶輸入的數據,驗證并保存
}
~~~
### 讀取默認值
你的表列也許定義了默認值。有時候,你可能需要在使用web表單的時候給AR預設一些值。如果你需要這樣做,可以在顯示表單內容前通過調用`loadDefaultValues()`方法來實現:?`````php $customer = new Customer(); $customer->loadDefaultValues(); // ... 渲染 $customer 的 HTML 表單 ...?`````
## AR的生命周期
理解AR的生命周期對于你操作數據庫非常重要。生命周期通常都會有些典型的事件存在。對于開發AR的behaviors來說非常有用。
當你實例化一個新的AR對象時,我們將獲得如下的生命周期:
1. constructor
2. yii\db\ActiveRecord::init(): 會觸發一個 yii\db\ActiveRecord::EVENT_INIT 事件
當你通過 yii\db\ActiveRecord::find() 方法查詢數據時,每個AR實例都將有以下生命周期:
1. constructor
2. yii\db\ActiveRecord::init(): 會觸發一個 yii\db\ActiveRecord::EVENT_INIT 事件
3. yii\db\ActiveRecord::afterFind(): 會觸發一個 yii\db\ActiveRecord::EVENT_AFTER_FIND 事件
當通過 yii\db\ActiveRecord::save() 方法寫入或者更新數據時, 我們將獲得如下生命周期:
1. yii\db\ActiveRecord::beforeValidate(): 會觸發一個 yii\db\ActiveRecord::EVENT_BEFORE_VALIDATE 事件
2. yii\db\ActiveRecord::afterValidate(): 會觸發一個 yii\db\ActiveRecord::EVENT_AFTER_VALIDATE 事件
3. yii\db\ActiveRecord::beforeSave(): 會觸發一個 yii\db\ActiveRecord::EVENT_BEFORE_INSERT 或 yii\db\ActiveRecord::EVENT_BEFORE_UPDATE 事件
4. 執行實際的數據寫入或更新
5. yii\db\ActiveRecord::afterSave(): 會觸發一個 yii\db\ActiveRecord::EVENT_AFTER_INSERT 或 yii\db\ActiveRecord::EVENT_AFTER_UPDATE 事件
最后,當調用 yii\db\ActiveRecord::delete() 刪除數據時, 我們將獲得如下生命周期:
1. yii\db\ActiveRecord::beforeDelete(): 會觸發一個 yii\db\ActiveRecord::EVENT_BEFORE_DELETE 事件
2. 執行實際的數據刪除
3. yii\db\ActiveRecord::afterDelete(): 會觸發一個 yii\db\ActiveRecord::EVENT_AFTER_DELETE 事件
## 查詢關聯的數據
使用 AR 方法也可以查詢數據表的關聯數據(如,選出表A的數據可以拉出表B的關聯數據)。 有了 AR, 返回的關聯數據連接就像連接關聯主表的 AR 對象的屬性一樣。
建立關聯關系后,通過?`$customer->orders`?可以獲取 一個?`Order`?對象的數組,該數組代表當前客戶對象的訂單集。
定義關聯關系使用一個可以返回 yii\db\ActiveQuery 對象的 getter 方法, yii\db\ActiveQuery對象有關聯上下文的相關信息,因此可以只查詢關聯數據。
例如:
~~~
class Customer extends \yii\db\ActiveRecord
{
public function getOrders()
{
// 客戶和訂單通過 Order.customer_id -> id 關聯建立一對多關系
return $this->hasMany(Order::className(), ['customer_id' => 'id']);
}
}
class Order extends \yii\db\ActiveRecord
{
// 訂單和客戶通過 Customer.id -> customer_id 關聯建立一對一關系
public function getCustomer()
{
return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
}
}
~~~
以上使用了 yii\db\ActiveRecord::hasMany() 和 yii\db\ActiveRecord::hasOne() 方法。 以上兩例分別是關聯數據多對一關系和一對一關系的建模范例。 如,一個客戶有很多訂單,一個訂單只歸屬一個客戶。 兩個方法都有兩個參數并返回 yii\db\ActiveQuery 對象。
* `$class`:關聯模型類名,它必須是一個完全合格的類名。
* `$link`: 兩個表的關聯列,應為鍵值對數組的形式。 數組的鍵是?`$class`?關聯表的列名, 而數組值是關聯類 $class 的列名。 基于表外鍵定義關聯關系是最佳方法。
建立關聯關系后,獲取關聯數據和獲取組件屬性一樣簡單, 執行以下相應getter方法即可:
~~~
// 取得客戶的訂單
$customer = Customer::findOne(1);
$orders = $customer->orders; // $orders 是 Order 對象數組
~~~
以上代碼實際執行了以下兩條 SQL 語句:
~~~
SELECT * FROM customer WHERE id=1;
SELECT * FROM order WHERE customer_id=1;
~~~
> 提示:再次用表達式?`$customer->orders`將不會執行第二次 SQL 查詢, SQL 查詢只在該表達式第一次使用時執行。 數據庫訪問只返回緩存在內部前一次取回的結果集,如果你想查詢新的 關聯數據,先要注銷現有結果集:`unset($customer->orders);`。
有時候需要在關聯查詢中傳遞參數,如不需要返回客戶全部訂單, 只需要返回購買金額超過設定值的大訂單, 通過以下getter方法聲明一個關聯數據?`bigOrders`?:
~~~
class Customer extends \yii\db\ActiveRecord
{
public function getBigOrders($threshold = 100)
{
return $this->hasMany(Order::className(), ['customer_id' => 'id'])
->where('subtotal > :threshold', [':threshold' => $threshold])
->orderBy('id');
}
}
~~~
`hasMany()`?返回 yii\db\ActiveQuery 對象,該對象允許你通過 yii\db\ActiveQuery 方法定制查詢。
如上聲明后,執行?`$customer->bigOrders`?就返回 總額大于100的訂單。使用以下代碼更改設定值:
~~~
$orders = $customer->getBigOrders(200)->all();
~~~
>注意:關聯查詢返回的是 yii\db\ActiveQuery 的實例,如果像特性(如類屬性)那樣連接關聯數據, 返回的結果是關聯查詢的結果,即 yii\db\ActiveRecord 的實例, 或者是數組,或者是 null ,取決于關聯關系的多樣性。如,`$customer->getOrders()`?返回`ActiveQuery`?實例,而?`$customer->orders`?返回`Order`?對象數組 (如果查詢結果為空則返回空數組)。
## 中間關聯表
有時,兩個表通過中間表關聯,定義這樣的關聯關系, 可以通過調用 yii\db\ActiveQuery::via() 方法或 yii\db\ActiveQuery::viaTable() 方法來定制 yii\db\ActiveQuery 對象 。
舉例而言,如果?`order`?表和?`item`?表通過中間表?`order_item`?關聯起來, 可以在?`Order`?類聲明?`items`?關聯關系取代中間表:
~~~
class Order extends \yii\db\ActiveRecord
{
public function getItems()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->viaTable('order_item', ['order_id' => 'id']);
}
}
~~~
兩個方法是相似的,除了 yii\db\ActiveQuery::via() 方法的第一個參數是使用 AR 類中定義的關聯名。 以上方法取代了中間表,等價于:
~~~
class Order extends \yii\db\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');
}
}
~~~
## 延遲加載和即時加載(又稱惰性加載與貪婪加載)
如前所述,當你第一次連接關聯對象時, AR 將執行一個數據庫查詢 來檢索請求數據并填充到關聯對象的相應屬性。 如果再次連接相同的關聯對象,不再執行任何查詢語句,這種數據庫查詢的執行方法稱為“延遲加載”。如:
~~~
// SQL executed: SELECT * FROM customer WHERE id=1
$customer = Customer::findOne(1);
// SQL executed: SELECT * FROM order WHERE customer_id=1
$orders = $customer->orders;
// 沒有 SQL 語句被執行
$orders2 = $customer->orders; //取回上次查詢的緩存數據
~~~
延遲加載非常實用,但是,在以下場景中使用延遲加載會遭遇性能問題:
~~~
// SQL executed: SELECT * FROM customer LIMIT 100
$customers = Customer::find()->limit(100)->all();
foreach ($customers as $customer) {
// SQL executed: SELECT * FROM order WHERE customer_id=...
$orders = $customer->orders;
// ...處理 $orders...
}
~~~
假設數據庫查出的客戶超過100個,以上代碼將執行多少條 SQL 語句? 101 條!第一條 SQL 查詢語句取回100個客戶,然后, 每個客戶要執行一條 SQL 查詢語句以取回該客戶的所有訂單。
為解決以上性能問題,可以通過調用 yii\db\ActiveQuery::with() 方法使用即時加載解決。
~~~
// SQL executed: SELECT * FROM customer LIMIT 100;
// SELECT * FROM orders WHERE customer_id IN (1,2,...)
$customers = Customer::find()->limit(100)
->with('orders')->all();
foreach ($customers as $customer) {
// 沒有 SQL 語句被執行
$orders = $customer->orders;
// ...處理 $orders...
}
~~~
如你所見,同樣的任務只需要兩個 SQL 語句。 >須知:通常,即時加載 N 個關聯關系而通過 via() 或者 viaTable() 定義了 M 個關聯關系, 將有 1+M+N 條 SQL 查詢語句被執行:一個查詢取回主表行數, 一個查詢給每一個 (M) 中間表,一個查詢給每個 (N) 關聯表。 注意:當用即時加載定制 select() 時,確保連接 到關聯模型的列都被包括了,否則,關聯模型不會載入。如:
~~~
$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer 總是空的,使用以下代碼解決這個問題:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();
~~~
有時候,你想自由的自定義關聯查詢,延遲加載和即時加載都可以實現,如:
~~~
$customer = Customer::findOne(1);
// 延遲加載: SELECT * FROM order WHERE customer_id=1 AND subtotal>100
$orders = $customer->getOrders()->where('subtotal>100')->all();
// 即時加載: SELECT * FROM customer LIMIT 100
// SELECT * FROM order WHERE customer_id IN (1,2,...) AND subtotal>100
$customers = Customer::find()->limit(100)->with([
'orders' => function($query) {
$query->andWhere('subtotal>100');
},
])->all();
~~~
## 逆關系
關聯關系通常成對定義,如:Customer 可以有個名為 orders 關聯項, 而 Order 也有個名為customer 的關聯項:
~~~
class Customer extends ActiveRecord
{
....
public function getOrders()
{
return $this->hasMany(Order::className(), ['customer_id' => 'id']);
}
}
class Order extends ActiveRecord
{
....
public function getCustomer()
{
return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
}
}
~~~
如果我們執行以下查詢,可以發現訂單的 customer 和 找到這些訂單的客戶對象并不是同一個。連接 customer->orders 將觸發一條 SQL 語句 而連接一個訂單的 customer 將觸發另一條 SQL 語句。
~~~
// SELECT * FROM customer WHERE id=1
$customer = Customer::findOne(1);
// 輸出 "不相同"
// SELECT * FROM order WHERE customer_id=1
// SELECT * FROM customer WHERE id=1
if ($customer->orders[0]->customer === $customer) {
echo '相同';
} else {
echo '不相同';
}
~~~
為避免多余執行的后一條語句,我們可以為 customer或 orders 關聯關系定義相反的關聯關系,通過調用 yii\db\ActiveQuery::inverseOf() 方法可以實現。
~~~
class Customer extends ActiveRecord
{
....
public function getOrders()
{
return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
}
}
~~~
現在我們同樣執行上面的查詢,我們將得到:
~~~
// SELECT * FROM customer WHERE id=1
$customer = Customer::findOne(1);
// 輸出相同
// SELECT * FROM order WHERE customer_id=1
if ($customer->orders[0]->customer === $customer) {
echo '相同';
} else {
echo '不相同';
}
~~~
以上我們展示了如何在延遲加載中使用相對關聯關系, 相對關系也可以用在即時加載中:
~~~
// SELECT * FROM customer
// SELECT * FROM order WHERE customer_id IN (1, 2, ...)
$customers = Customer::find()->with('orders')->all();
// 輸出相同
if ($customers[0]->orders[0]->customer === $customers[0]) {
echo '相同';
} else {
echo '不相同';
}
~~~
>注意:相對關系不能在包含中間表的關聯關系中定義。 即是,如果你的關系是通過yii\db\ActiveQuery::via() 或 yii\db\ActiveQuery::viaTable()方法定義的, 就不能調用yii\db\ActiveQuery::inverseOf()方法了。
## JOIN 類型關聯查詢
使用關系數據庫時,普遍要做的是連接多個表并明確地運用各種 JOIN 查詢。 JOIN SQL語句的查詢條件和參數,使用 yii\db\ActiveQuery::joinWith() 可以重用已定義關系并調用 而不是使用 yii\db\ActiveQuery::join() 來實現目標。
~~~
// 查找所有訂單并以客戶 ID 和訂單 ID 排序,并貪婪加載 "customer" 表
$orders = Order::find()->joinWith('customer')->orderBy('customer.id, order.id')->all();
// 查找包括書籍的所有訂單,并以 `INNER JOIN` 的連接方式即時加載 "books" 表
$orders = Order::find()->innerJoinWith('books')->all();
~~~
以上,方法 yii\db\ActiveQuery::innerJoinWith() 是訪問?`INNER JOIN`?類型的 yii\db\ActiveQuery::joinWith() 的快捷方式。
可以連接一個或多個關聯關系,可以自由使用查詢條件到關聯查詢, 也可以嵌套連接關聯查詢。如:
~~~
// 連接多重關系
// 找出24小時內注冊客戶包含書籍的訂單
$orders = Order::find()->innerJoinWith([
'books',
'customer' => function ($query) {
$query->where('customer.created_at > ' . (time() - 24 * 3600));
}
])->all();
// 連接嵌套關系:連接 books 表及其 author 列
$orders = Order::find()->joinWith('books.author')->all();
~~~
代碼背后, Yii 先執行一條 JOIN SQL 語句把滿足 JOIN SQL 語句查詢條件的主要模型查出, 然后為每個關系執行一條查詢語句, bing填充相應的關聯記錄。
yii\db\ActiveQuery::joinWith() 和 yii\db\ActiveQuery::with() 的區別是 前者連接主模型類和關聯模型類的數據表來檢索主模型, 而后者只查詢和檢索主模型類。 檢索主模型
由于這個區別,你可以應用只針對一條 JOIN SQL 語句起效的查詢條件。 如,通過關聯模型的查詢條件過濾主模型,如前例, 可以使用關聯表的列來挑選主模型數據,
當使用 yii\db\ActiveQuery::joinWith() 方法時可以響應沒有歧義的列名。 In the above examples, we use?`item.id`?and?`order.id`?to disambiguate the?`id`?column references 因為訂單表和項目表都包括?`id`?列。
當連接關聯關系時,關聯關系默認使用即時加載。你可以 通過傳參數?`$eagerLoading`?來決定在指定關聯查詢中是否使用即時加載。
默認 yii\db\ActiveQuery::joinWith() 使用左連接來連接關聯表。 你也可以傳?`$joinType`?參數來定制連接類型。 你也可以使用 yii\db\ActiveQuery::innerJoinWith()。
以下是?`INNER JOIN`?的簡短例子:
~~~
// 查找包括書籍的所有訂單,但 "books" 表不使用即時加載
$orders = Order::find()->innerJoinWith('books', false)->all();
// 等價于:
$orders = Order::find()->joinWith('books', false, 'INNER JOIN')->all();
~~~
有時連接兩個表時,需要在關聯查詢的 ON 部分指定額外條件。 這可以通過調用 yii\db\ActiveQuery::onCondition() 方法實現:
~~~
class User extends ActiveRecord
{
public function getBooks()
{
return $this->hasMany(Item::className(), ['owner_id' => 'id'])->onCondition(['category_id' => 1]);
}
}
~~~
在上面, yii\db\ActiveRecord::hasMany() 方法回傳了一個 yii\db\ActiveQuery 對象, 當你用 yii\db\ActiveQuery::joinWith() 執行一條查詢時,取決于正被調用的是哪個 yii\db\ActiveQuery::onCondition(), 返回?`category_id`?為 1 的 items
當你用 yii\db\ActiveQuery::joinWith() 進行一次查詢時,“on-condition”條件會被放置在相應查詢語句的 ON 部分, 如:
~~~
// SELECT user.* FROM user LEFT JOIN item ON item.owner_id=user.id AND category_id=1
// SELECT * FROM item WHERE owner_id IN (...) AND category_id=1
$users = User::find()->joinWith('books')->all();
~~~
注意:如果通過 yii\db\ActiveQuery::with() 進行貪婪加載或使用惰性加載的話,則 on 條件會被放置在對應 SQL語句的?`WHERE`?部分。 因為,此時此處并沒有發生 JOIN 查詢。比如:
~~~
// SELECT * FROM user WHERE id=10
$user = User::findOne(10);
// SELECT * FROM item WHERE owner_id=10 AND category_id=1
$books = $user->books;
~~~
## 關聯表操作
AR 提供了下面兩個方法用來建立和解除兩個關聯對象之間的關系:
* yii\db\ActiveRecord::link()
* yii\db\ActiveRecord::unlink()
例如,給定一個customer和order對象,我們可以通過下面的代碼使得customer對象擁有order對象:
~~~
$customer = Customer::findOne(1);
$order = new Order();
$order->subtotal = 100;
$customer->link('orders', $order);
~~~
yii\db\ActiveRecord::link() 調用上述將設置 customer_id 的順序是 $customer 的主鍵值,然后調用 yii\db\ActiveRecord::save() 要將順序保存到數據庫中。
## 作用域
當你調用yii\db\ActiveRecord::find() 或 yii\db\ActiveRecord::findBySql()方法時,將會返回一個yii\db\ActiveQuery實例。之后,你可以調用其他查詢方法,如 yii\db\ActiveQuery::where(),yii\db\ActiveQuery::orderBy(), 進一步的指定查詢條件。
有時候你可能需要在不同的地方使用相同的查詢方法。如果出現這種情況,你應該考慮定義所謂的作用域。作用域是本質上要求一組的查詢方法來修改查詢對象的自定義查詢類中定義的方法。 之后你就可以像使用普通方法一樣使用作用域。
只需兩步即可定義一個作用域。首先給你的model創建一個自定義的查詢類,在此類中定義的所需的范圍方法。例如,給Comment模型創建一個 CommentQuery類,然后在CommentQuery類中定義一個active()的方法為作用域,像下面的代碼:
~~~
namespace app\models;
use yii\db\ActiveQuery;
class CommentQuery extends ActiveQuery
{
public function active($state = true)
{
$this->andWhere(['active' => $state]);
return $this;
}
}
~~~
重點:
1. 類必須繼承 yii\db\ActiveQuery (或者是其他的 ActiveQuery ,比如 yii\mongodb\ActiveQuery)。
2. 必須是一個public類型的方法且必須返回 $this 實現鏈式操作。可以傳入參數。
3. 檢查 yii\db\ActiveQuery 對于修改查詢條件是非常有用的方法。
其次,覆蓋yii\db\ActiveRecord::find() 方法使其返回自定義的查詢對象而不是常規的yii\db\ActiveQuery。對于上述例子,你需要編寫如下代碼:
~~~
namespace app\models;
use yii\db\ActiveRecord;
class Comment extends ActiveRecord
{
/**
* @inheritdoc
* @return CommentQuery
*/
public static function find()
{
return new CommentQuery(get_called_class());
}
}
~~~
就這樣,現在你可以使用自定義的作用域方法了:
~~~
$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();
~~~
你也能在定義的關聯里使用作用域方法,比如:
~~~
class Post extends \yii\db\ActiveRecord
{
public function getActiveComments()
{
return $this->hasMany(Comment::className(), ['post_id' => 'id'])->active();
}
}
~~~
或者在執行關聯查詢的時候使用(on-the-fly 是啥?):
~~~
$posts = Post::find()->with([
'comments' => function($q) {
$q->active();
}
])->all();
~~~
### 默認作用域
如果你之前用過 Yii 1.1 就應該知道默認作用域的概念。一個默認的作用域可以作用于所有查詢。你可以很容易的通過重寫yii\db\ActiveRecord::find()方法來定義一個默認作用域,例如:
~~~
public static function find()
{
return parent::find()->where(['deleted' => false]);
}
~~~
注意,你之后所有的查詢都不能用 yii\db\ActiveQuery::where(),但是可以用 yii\db\ActiveQuery::andWhere() 和 yii\db\ActiveQuery::orWhere(),他們不會覆蓋掉默認作用域。(譯注:如果你要使用默認作用域,就不能在 xxx::find()后使用where()方法,你必須使用andXXX()或者orXXX()系的方法,否則默認作用域不會起效果,至于原因,打開where()方法的代碼一看便知)
## 事務操作
當執行幾個相關聯的數據庫操作的時候
TODO: FIXME: WIP, TBD,?[https://github.com/yiisoft/yii2/issues/226](https://github.com/yiisoft/yii2/issues/226)
, yii\db\ActiveRecord::afterSave(), yii\db\ActiveRecord::beforeDelete() and/or yii\db\ActiveRecord::afterDelete() 生命周期周期方法(life cycle methods 我覺得這句翻譯成“模板方法”會不會更好點?)。開發者可以通過重寫yii\db\ActiveRecord::save()方法然后在控制器里使用事務操作,嚴格地說是似乎不是一個好的做法 (召回"瘦控制器 / 肥模型"基本規則)。
這些方法在這里(如果你不明白自己實際在干什么,請不要使用他們),Models:
~~~
class Feature extends \yii\db\ActiveRecord
{
// ...
public function getProduct()
{
return $this->hasOne(Product::className(), ['id' => 'product_id']);
}
}
class Product extends \yii\db\ActiveRecord
{
// ...
public function getFeatures()
{
return $this->hasMany(Feature::className(), ['product_id' => 'id']);
}
}
~~~
重寫 yii\db\ActiveRecord::save() 方法:
~~~
class ProductController extends \yii\web\Controller
{
public function actionCreate()
{
// FIXME: TODO: WIP, TBD
}
}
~~~
(譯注:我覺得上面應該是原手冊里的bug)
在控制器層使用事務:
~~~
class ProductController extends \yii\web\Controller
{
public function actionCreate()
{
// FIXME: TODO: WIP, TBD
}
}
~~~
作為這些脆弱方法的替代,你應該使用原子操作方案特性。
~~~
class Feature extends \yii\db\ActiveRecord
{
// ...
public function getProduct()
{
return $this->hasOne(Product::className(), ['product_id' => 'id']);
}
public function scenarios()
{
return [
'userCreates' => [
'attributes' => ['name', 'value'],
'atomic' => [self::OP_INSERT],
],
];
}
}
class Product extends \yii\db\ActiveRecord
{
// ...
public function getFeatures()
{
return $this->hasMany(Feature::className(), ['id' => 'product_id']);
}
public function scenarios()
{
return [
'userCreates' => [
'attributes' => ['title', 'price'],
'atomic' => [self::OP_INSERT],
],
];
}
public function afterValidate()
{
parent::afterValidate();
// FIXME: TODO: WIP, TBD
}
public function afterSave($insert)
{
parent::afterSave($insert);
if ($this->getScenario() === 'userCreates') {
// FIXME: TODO: WIP, TBD
}
}
}
~~~
Controller里的代碼將變得很簡潔:
~~~
class ProductController extends \yii\web\Controller
{
public function actionCreate()
{
// FIXME: TODO: WIP, TBD
}
}
~~~
控制器非常簡潔:
~~~
class ProductController extends \yii\web\Controller
{
public function actionCreate()
{
// FIXME: TODO: WIP, TBD
}
}
~~~
## 樂觀鎖(Optimistic Locks)
TODO
## 被污染屬性
當你調用yii\db\ActiveRecord::save()用于保存活動記錄(Active Record)實例時,只有被污染的屬性才會被保存。一個屬性是否認定為被污染取決于它的值自從最后一次從數據庫加載或者最近一次保存到數據庫后到現在是否被修改過。注意:無論活動記錄(Active Record)是否有被污染屬性,數據驗證始終會執行。
活動記錄(Active Record)會自動維護一個污染數據列表。它的工作方式是通過維護一個較舊屬性值版本,并且將它們與最新的進行比較。你可以通過調用yii\db\ActiveRecord::getDirtyAttributes()來獲取當前的污染屬性。你也可以調用yii\db\ActiveRecord::markAttributeDirty()來顯示的標記一個屬性為污染屬性。
如果你對最近一次修改前的屬性值感興趣,你可以調用yii\db\ActiveRecord::getOldAttributes() 或 yii\db\ActiveRecord::getOldAttribute()。
## 另見
* [模型(Model)](http://www.yiichina.com/doc/guide/2.0/model)
* yii\db\ActiveRecord
- 介紹(Introduction)
- 關于 Yii(About Yii)
- 從 Yii 1.1 升級(Upgrading from Version 1.1)
- 入門(Getting Started)
- 安裝 Yii(Installing Yii)
- 運行應用(Running Applications)
- 第一次問候(Saying Hello)
- 使用 Forms(Working with Forms)
- 玩轉 Databases(Working with Databases)
- 用 Gii 生成代碼(Generating Code with Gii)
- 更上一層樓(Looking Ahead)
- 應用結構(Application Structure)
- 結構概述(Overview)
- 入口腳本(Entry Scripts)
- 應用(Applications)
- 應用組件(Application Components)
- 控制器(Controllers)
- 模型(Models)
- 視圖(Views)
- 模塊(Modules)
- 過濾器(Filters)
- 小部件(Widgets)
- 前端資源(Assets)
- 擴展(Extensions)
- 請求處理(Handling Requests)
- 運行概述(Overview)
- 引導(Bootstrapping)
- 路由引導與創建 URL(Routing and URL Creation)
- 請求(Requests)
- 響應(Responses)
- Sessions and Cookies
- 錯誤處理(Handling Errors)
- 日志(Logging)
- 關鍵概念(Key Concepts)
- 組件(Components)
- 屬性(Properties)
- 事件(Events)
- 行為(Behaviors)
- 配置(Configurations)
- 別名(Aliases)
- 類自動加載(Class Autoloading)
- 服務定位器(Service Locator)
- 依賴注入容器(Dependency Injection Container)
- 配合數據庫工作(Working with Databases)
- 數據庫訪問(Data Access Objects): 數據庫連接、基本查詢、事務和模式操作
- 查詢生成器(Query Builder): 使用簡單抽象層查詢數據庫
- 活動記錄(Active Record): 活動記錄對象關系映射(ORM),檢索和操作記錄、定義關聯關系
- 數據庫遷移(Migrations): 在團體開發中對你的數據庫使用版本控制
- Sphinx
- Redis
- MongoDB
- ElasticSearch
- 接收用戶數據(Getting Data from Users)
- 創建表單(Creating Forms)
- 輸入驗證(Validating Input)
- 文件上傳(Uploading Files)
- 收集列表輸入(Collecting Tabular Input)
- 多模型同時輸入(Getting Data for Multiple Models)
- 顯示數據(Displaying Data)
- 格式化輸出數據(Data Formatting)
- 分頁(Pagination)
- 排序(Sorting)
- 數據提供器(Data Providers)
- 數據小部件(Data Widgets)
- 操作客戶端腳本(Working with Client Scripts)
- 主題(Theming)
- 安全(Security)
- 認證(Authentication)
- 授權(Authorization)
- 處理密碼(Working with Passwords)
- 客戶端認證(Auth Clients)
- 安全領域的最佳實踐(Best Practices)
- 緩存(Caching)
- 概述(Overview)
- 數據緩存(Data Caching)
- 片段緩存(Fragment Caching)
- 分頁緩存(Page Caching)
- HTTP 緩存(HTTP Caching)
- RESTful Web 服務
- 快速入門(Quick Start)
- 資源(Resources)
- 控制器(Controllers)
- 路由(Routing)
- 格式化響應(Response Formatting)
- 授權驗證(Authentication)
- 速率限制(Rate Limiting)
- 版本化(Versioning)
- 錯誤處理(Error Handling)
- 開發工具(Development Tools)
- 調試工具欄和調試器(Debug Toolbar and Debugger)
- 使用 Gii 生成代碼(Generating Code using Gii)
- TBD 生成 API 文檔(Generating API Documentation)
- 測試(Testing)
- 概述(Overview)
- 搭建測試環境(Testing environment setup)
- 單元測試(Unit Tests)
- 功能測試(Functional Tests)
- 驗收測試(Acceptance Tests)
- 測試夾具(Fixtures)
- 高級專題(Special Topics)
- 高級應用模版(Advanced Project Template)
- 從頭構建自定義模版(Building Application from Scratch)
- 控制臺命令(Console Commands)
- 核心驗證器(Core Validators)
- 國際化(Internationalization)
- 收發郵件(Mailing)
- 性能優化(Performance Tuning)
- 共享主機環境(Shared Hosting Environment)
- 模板引擎(Template Engines)
- 集成第三方代碼(Working with Third-Party Code)
- 小部件(Widgets)
- Bootstrap 小部件(Bootstrap Widgets)
- jQuery UI 小部件(jQuery UI Widgets)
- 助手類(Helpers)
- 助手一覽(Overview)
- Array 助手(ArrayHelper)
- Html 助手(Html)
- Url 助手(Url)