Yii的修行之路 - Active Record 活動記錄

簡介

Yii 在操做數據庫方面提供了一個十分強大的類庫來支撐整個框架業務的運轉,這就是 Active Record (活動記錄,如下簡稱AR)。php

基本概念

AR類提供了一個面向對象的接口, 用以訪問數據庫中的數據。mysql

例如,假定 Customer AR 類關聯着 customer 表,且該類的 name 屬性表明 customer 表的 name 列。 你能夠寫如下代碼來哉customer 表裏插入一行新的記錄:ios

用 AR 而不是原生的 SQL 語句去執行數據庫查詢,能夠調用直觀方法來實現相同目標。git

如,調用 yiidbActiveRecord::save() 方法將執行插入或更新輪詢,將在該 AR 類關聯的數據表新建或更新一行數據:github

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();  // 一行新數據插入 customer 表

上面的代碼和使用下面的原生 SQL 語句是等效的,但顯然前者更直觀, 更不易出錯,而且面對不一樣的數據庫系統(DBMS, Database Management System)時更不容易產生兼容性問題。web

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

下面是全部目前被 Yii 的 AR 功能所支持的數據庫列表:redis

  • MySQL 4.1 及以上:經過 yiidbActiveRecordsql

  • PostgreSQL 7.3 及以上:經過 yiidbActiveRecordmongodb

  • SQLite 2 和 3:經過 yiidbActiveRecord數據庫

  • Microsoft SQL Server 2010 及以上:經過 yiidbActiveRecord

  • Oracle: 經過 yiidbActiveRecord

  • CUBRID 9.1 及以上:經過 yiidbActiveRecord

  • Sphinx:經過 yiisphinxActiveRecord,需求 yii2-sphinx 擴展

  • ElasticSearch:經過 yiielasticsearchActiveRecord,需求 yii2-elasticsearch 擴展

  • Redis 2.6.12 及以上:經過 yiiredisActiveRecord,需求 yii2-redis 擴展

  • MongoDB 1.3.0 及以上:經過 yiimongodbActiveRecord,需求 yii2-mongodb 擴展

如你所見,Yii 不只提供了對關係型數據庫的 AR 支持,還提供了 NoSQL 數據庫的支持。

聲明 AR 類

要想聲明一個 AR 類,你須要擴展 yiidbActiveRecord 基類, 並實現 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 用一個 yiidbConnection 對象與數據庫交換數據。

默認的,它使用 db 組件做爲其鏈接對象,你能夠在應用程序配置文件中設置下 db 組件,就像這樣:

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

若是在你的應用中應用了不止一個數據庫,且你須要給你的 AR 類使用不一樣的數據庫連接(DB connection) ,你能夠覆蓋掉 yiidbActiveRecord::getDb() 方法:

class Customer extends ActiveRecord{
    // ...
    public static function getDb()
    {
        return \Yii::$app->db2;  // 使用名爲 "db2" 的應用組件
    }
}

查詢數據

AR 提供了兩種方法來構建 DB 查詢並向 AR 實例裏填充數據:

  • yiidbActiveRecord::find()

  • yiidbActiveRecord::findBySql()

以上兩個方法都會返回 yiidbActiveQuery 實例,該類繼承自yiidbQuery, 所以,他們都支持同一套靈活且強大的 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 數組以 ID 爲索引
$customers = Customer::find()->indexBy('id')->all();

// 用原生 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爲一、二、3的一組客戶
$customers = Customer::findAll([1, 2, 3]);

// 返回全部狀態爲 "deleted" 的客戶
$customer = Customer::findAll([
    'status' => Customer::STATUS_DELETED,
]);

以數組形式獲取數據

有時候,咱們須要處理很大量的數據,這時可能須要用一個數組來存儲取到的數據, 從而節省內存。

你能夠用 asArray() 函數作到這一點:

// 以數組而不是對象形式取回客戶信息:
$customers = Customer::find()
    ->asArray()
    ->all();
// $customers 的每一個元素都是鍵值對數組

批量獲取數據

在 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) {

}

操做數據(CURD)

AR 提供如下方法插入、更新和刪除與 AR 對象關聯的那張表中的某一行:

  • yiidbActiveRecord::save()

  • yiidbActiveRecord::insert()

  • yiidbActiveRecord::update()

  • yiidbActiveRecord::delete()

AR 同時提供了一些靜態方法,能夠應用在與某 AR 類所關聯的整張表上。

用這些方法的時候千萬要當心,由於他們做用於整張表!

好比,deleteAll() 會刪除掉表裏全部的記錄。

  • yiidbActiveRecord::updateCounters()

  • yiidbActiveRecord::updateAll()

  • yiidbActiveRecord::updateAllCounters()

  • yiidbActiveRecord::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 對象是否是新對象(在函數內部,他會檢查 yiidbActiveRecord::isNewRecord 的值)。

若 AR 對象是由 new 操做符 初始化出來的,save() 方法會在表裏插入一條數據; 若是一個 AR 是由 find() 方法獲取來的, 則 save() 會更新表裏的對應行記錄。

數據輸入與有效性驗證

因爲AR繼承自yiibaseModel,因此它一樣也支持Model的數據輸入、驗證等特性。

例如,你能夠聲明一個rules方法用來覆蓋掉yiibaseModel::rules()裏的;你也能夠給AR實例批量賦值;你也能夠經過調用yiibaseModel::validate()執行數據驗證。

當你調用 save()、insert()、update() 這三個方法時,會自動調用yiibaseModel::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 事件

當你經過 yiidbActiveRecord::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 事件

當經過 yiidbActiveRecord::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 事件

最後,當調用 yiidbActiveRecord::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 對象的數組,該數組表明當前客戶對象的訂單集。

定義關聯關係使用一個能夠返回 yiidbActiveQuery 對象的 getter 方法, yiidbActiveQuery對象有關聯上下文的相關信息,所以能夠只查詢關聯數據。

例如:

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']); 
    } 
}

以上使用了 yiidbActiveRecord::hasMany() 和 yiidbActiveRecord::hasOne() 方法。

以上兩例分別是關聯數據多對一關係和一對一關係的建模範例。

如,一個客戶有不少訂單,一個訂單隻歸屬一個客戶。

兩個方法都有兩個參數並返回 yiidbActiveQuery 對象。

$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() 返回 yiidbActiveQuery 對象,該對象容許你經過 yiidbActiveQuery 方法定製查詢。

如上聲明後,執行 $customer->bigOrders 就返回 總額大於100的訂單。使用如下代碼更改設定值:

$orders = $customer->getBigOrders(200)->all();

注意:關聯查詢返回的是 yiidbActiveQuery 的實例,若是像特性(如類屬性)那樣鏈接關聯數據, 返回的結果是關聯查詢的結果,即 yiidbActiveRecord 的實例, 或者是數組,或者是 null ,取決於關聯關係的多樣性。

如,$customer->getOrders() 返回ActiveQuery 實例,而 $customer->orders 返回Order 對象數組 (若是查詢結果爲空則返回空數組)。

中間關聯表

有時,兩個表經過中間表關聯,定義這樣的關聯關係, 能夠經過調用 yiidbActiveQuery::via() 方法或 yiidbActiveQuery::viaTable() 方法來定製 yiidbActiveQuery 對象 。

舉例而言,若是 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']); 
    } 
}

兩個方法是類似的,除了 yiidbActiveQuery::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 查詢語句以取回該客戶的全部訂單。

爲解決以上性能問題,能夠經過調用 yiidbActiveQuery::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 關聯關係定義相反的關聯關係,經過調用 yiidbActiveQuery::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語句的查詢條件和參數,使用 yiidbActiveQuery::joinWith() 能夠重用已定義關係並調用 而不是使用 yiidbActiveQuery::join() 來實現目標。

// 查找全部訂單並以客戶 ID 和訂單 ID 排序,並貪婪加載 "customer" 表
$orders = Order::find()
    ->joinWith('customer')
    ->orderBy('customer.id, order.id')
    ->all();
// 查找包括書籍的全部訂單,並以 `INNER JOIN` 的鏈接方式即時加載 "books" 表
$orders = Order::find()
    ->innerJoinWith('books')
    ->all();

以上,方法 yiidbActiveQuery::innerJoinWith() 是訪問 INNER JOIN 類型的 yiidbActiveQuery::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() 方法時能夠響應沒有歧義的列名。
 
當鏈接關聯關係時,關聯關係默認使用即時加載。

你能夠 經過傳參數 $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 部分指定額外條件。

這能夠經過調用 yiidbActiveQuery::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。

當你用 yiidbActiveQuery::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();

注意:若是經過 yiidbActiveQuery::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 提供了下面兩個方法用來創建和解除兩個關聯對象之間的關係:

  • yiidbActiveRecord::link()

  • yiidbActiveRecord::unlink()

例如,給定一個customer和order對象,咱們能夠經過下面的代碼使得customer對象擁有order對象:

$customer = Customer::findOne(1); $order = new Order(); $order->subtotal = 100; $customer->link('orders', $order);

yiidbActiveRecord::link() 調用上述將設置 customer_id 的順序是 $customer 的主鍵值,而後調用 yiidbActiveRecord::save() 要將順序保存到數據庫中。

做用域

當你調用yiidbActiveRecord::find() 或 yiidbActiveRecord::findBySql()方法時,將會返回一個yiidbActiveQuery實例。

以後,你能夠調用其餘查詢方法,如 yiidbActiveQuery::where(),yiidbActiveQuery::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. 類必須繼承 yiidbActiveQuery (或者是其餘的 ActiveQuery ,好比 yiimongodbActiveQuery)。

  2. 必須是一個public類型的方法且必須返回 $this 實現鏈式操做。能夠傳入參數。

  3. 檢查 yiidbActiveQuery 對於修改查詢條件是很是有用的方法。

其次,覆蓋yiidbActiveRecord::find() 方法使其返回自定義的查詢對象而不是常規的yiidbActiveQuery。

對於上述例子,你須要編寫以下代碼:

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 就應該知道默認做用域的概念。

一個默認的做用域能夠做用於全部查詢。

你能夠很容易的經過重寫yiidbActiveRecord::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
, yii\db\ActiveRecord::afterSave(), yii\db\ActiveRecord::beforeDelete() and/or yii\db\ActiveRecord::afterDelete()

生命週期週期方法(life cycle methods 我以爲這句翻譯成「模板方法」會不會更好點?)。

開發者能夠經過重寫yiidbActiveRecord::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']); 
    } 
}

重寫 yiidbActiveRecord::save() 方法:

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
    }
}

做爲這些脆弱方法的替代,你應該使用原子操做方案特性。

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
    }
}

被污染屬性

當你調用yiidbActiveRecord::save()用於保存活動記錄(Active Record)實例時,只有被污染的屬性纔會被保存。

一個屬性是否定定爲被污染取決於它的值自從最後一次從數據庫加載或者最近一次保存到數據庫後到如今是否被修改過。注意:不管活動記錄(Active Record)是否有被污染屬性,數據驗證始終會執行。

活動記錄(Active Record)會自動維護一個污染數據列表。它的工做方式是經過維護一個較舊屬性值版本,而且將它們與最新的進行比較。

你能夠經過調用yiidbActiveRecord::getDirtyAttributes()來獲取當前的污染屬性。

你也能夠調用yiidbActiveRecord::markAttributeDirty()來顯示的標記一個屬性爲污染屬性。

若是你對最近一次修改前的屬性值感興趣,你能夠調用yiidbActiveRecord::getOldAttributes() 或 yiidbActiveRecord::getOldAttribute()。

相關文章
相關標籤/搜索