16 追蹤者

Active Record

Active Record 提供物件導向介面,用於存取和操作儲存在資料庫中的資料。Active Record 類別與資料庫表相關聯,Active Record 實例對應於該表的一列,而 Active Record 實例的屬性表示該列中特定欄位的值。您可以使用存取 Active Record 屬性和呼叫 Active Record 方法,來存取和操作儲存在資料庫表中的資料,而無需編寫原始 SQL 語句。

例如,假設 Customer 是一個 Active Record 類別,它與 customer 表相關聯,而 namecustomer 表的欄位。您可以編寫以下程式碼,將新列插入到 customer 表中

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

上述程式碼等同於對 MySQL 使用以下原始 SQL 語句,但原始 SQL 語句較不直觀、更容易出錯,如果您使用的是不同類型的資料庫,甚至可能會有相容性問題

$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(請注意,由於 cubrid PDO 擴展中的 錯誤,值的引用將無法運作,因此您需要 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 在關聯式資料庫中的用法。但是,此處描述的大部分內容也適用於 NoSQL 資料庫的 Active Record。

宣告 Active Record 類別

若要開始使用,請宣告 Active Record 類別,方法是擴展 yii\db\ActiveRecord

設定資料表名稱

預設情況下,每個 Active Record 類別都與其資料庫表相關聯。tableName() 方法會透過 yii\helpers\Inflector::camel2id() 轉換類別名稱來傳回資料表名稱。如果資料表的命名不是依照此慣例,您可以覆寫此方法。

也可以套用預設的 tablePrefix。例如,如果 tablePrefixtbl_,則 Customer 會變成 tbl_customer,而 OrderItem 會變成 tbl_order_item

如果資料表名稱是以 {{%TableName}} 給定,則百分比字元 % 將會被資料表前綴取代。例如,{{%post}} 會變成 {{tbl_post}}。資料表名稱周圍的括號用於 在 SQL 查詢中引用

在以下範例中,我們為 customer 資料庫表宣告一個名為 Customer 的 Active Record 類別。

namespace app\models;

use yii\db\ActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
    
    /**
     * @return string the name of the table associated with this ActiveRecord class.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Active Record 被稱為「模型」

Active Record 實例被視為模型。因此,我們通常將 Active Record 類別放在 app\models 命名空間下(或其他用於存放模型類別的命名空間)。

由於 yii\db\ActiveRecord 擴展自 yii\base\Model,因此它繼承了所有模型功能,例如屬性、驗證規則、資料序列化等等。

連線到資料庫

預設情況下,Active Record 使用 db 應用程式組件 作為 DB 連線,以存取和操作資料庫資料。如 資料庫存取物件 中所述,您可以在應用程式組態中設定 db 組件,如下所示:

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

如果您想要使用 db 組件以外的其他資料庫連線,您應該覆寫 getDb() 方法

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // use the "db2" application component
        return \Yii::$app->db2;  
    }
}

查詢資料

宣告 Active Record 類別之後,您可以使用它從對應的資料庫表查詢資料。此程序通常包含以下三個步驟:

  1. 透過呼叫 yii\db\ActiveRecord::find() 方法建立新的查詢物件;
  2. 透過呼叫 查詢建構方法 來建構查詢物件;
  3. 呼叫查詢方法以 Active Record 實例的形式擷取資料。

如您所見,這與查詢產生器的程序非常相似。唯一的區別在於,您不是使用 new 運算子來建立查詢物件,而是呼叫 yii\db\ActiveRecord::find() 來傳回新的查詢物件,該物件的類別為 yii\db\ActiveQuery

以下是一些範例,說明如何使用 Active Query 查詢資料

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

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

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

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

在上述程式碼中,$customer 是一個 Customer 物件,而 $customers 是一個 Customer 物件陣列。它們都使用從 customer 表擷取的資料填入。

資訊:由於 yii\db\ActiveQuery 擴展自 yii\db\Query,因此您可以使用查詢產生器章節中描述的所有查詢建構方法和查詢方法。

由於依主要索引鍵值或一組欄位值查詢是很常見的任務,因此 Yii 提供了兩種用於此目的的捷徑方法:

這兩種方法都可以採用下列參數格式之一:

  • 純量值:該值被視為要尋找的所需主要索引鍵值。Yii 將透過讀取資料庫結構描述資訊自動判斷哪個欄位是主要索引鍵欄位。
  • 純量值陣列:該陣列被視為要尋找的所需主要索引鍵值。
  • 關聯式陣列:鍵是欄位名稱,值是要尋找的對應所需欄位值。請參閱 雜湊格式 以取得更多詳細資訊。

以下程式碼示範如何使用這些方法:

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

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

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

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

警告:如果您需要將使用者輸入傳遞給這些方法,請確保輸入值是純量值,或者在陣列條件的情況下,請確保陣列結構無法從外部變更

// yii\web\Controller ensures that $id is scalar
public function actionView($id)
{
    $model = Post::findOne($id);
    // ...
}

// explicitly specifying the column to search, passing a scalar or array here will always result in finding a single record
$model = Post::findOne(['id' => Yii::$app->request->get('id')]);

// do NOT use the following code! it is possible to inject an array condition to filter by arbitrary column values!
$model = Post::findOne(Yii::$app->request->get('id'));

注意: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() 方法來執行此操作

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

在呼叫 findBySql() 之後,請勿呼叫額外的查詢建構方法,因為它們會被忽略。

存取資料

如前所述,從資料庫傳回的資料會填入 Active Record 實例,而查詢結果的每一列都對應於單一 Active Record 實例。您可以透過存取 Active Record 實例的屬性來存取欄位值,例如:

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

注意:Active Record 屬性是以區分大小寫的方式,依關聯資料表欄位命名的。Yii 會自動在 Active Record 中為關聯資料表的每個欄位定義一個屬性。您不應重新宣告任何屬性。

由於 Active Record 屬性是以資料表欄位命名的,您可能會發現您正在編寫類似 $customer->first_name 的 PHP 程式碼,如果您的資料表欄位是以這種方式命名,則會在屬性名稱中使用底線分隔單字。如果您擔心程式碼樣式一致性,您應該相應地重新命名您的資料表欄位(例如,使用 camelCase)。

資料轉換

經常發生的情況是,輸入和/或顯示的資料格式與儲存在資料庫中的資料格式不同。例如,在資料庫中,您將客戶的生日儲存為 UNIX 時間戳記(雖然這不是一個好的設計),但在大多數情況下,您會希望以 'YYYY/MM/DD' 格式的字串來操作生日。為了達到此目的,您可以在 Customer Active Record 類別中定義資料轉換方法,如下所示:

class Customer extends ActiveRecord
{
    // ...

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

現在在您的 PHP 程式碼中,您會存取 $customer->birthdayText,而不是存取 $customer->birthday,這將允許您以 'YYYY/MM/DD' 格式輸入和顯示客戶生日。

提示:以上範例顯示了轉換不同格式資料的一般方式。如果您正在使用日期值,您可以使用 DateValidatoryii\jui\DatePicker,它們更易於使用且功能更強大。

以陣列方式擷取資料

雖然以 Active Record 物件的形式擷取資料既方便又靈活,但當您必須傳回大量資料時,由於記憶體佔用量大,因此並不總是理想的。在這種情況下,您可以透過在執行查詢方法之前呼叫 asArray(),使用 PHP 陣列來擷取資料

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

注意:雖然此方法可以節省記憶體並提高效能,但它更接近較低的 DB 抽象層,並且您將會遺失大多數 Active Record 功能。一個非常重要的區別在於欄位值的資料類型。當您以 Active Record 實例傳回資料時,欄位值會根據實際欄位類型自動進行型別轉換;另一方面,當您以陣列傳回資料時,欄位值將會是字串(因為它們是 PDO 的結果,沒有經過任何處理),無論它們的實際欄位類型為何。

批次擷取資料

查詢產生器 中,我們已說明您可以使用批次查詢,在從資料庫查詢大量資料時,將記憶體使用量降至最低。您可以在 Active Record 中使用相同的技術。例如:

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

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

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

儲存資料

使用 Active Record,您可以輕鬆地將資料儲存到資料庫,方法是執行以下步驟:

  1. 準備一個 Active Record 實例
  2. 為 Active Record 屬性指派新值
  3. 呼叫 yii\db\ActiveRecord::save() 以將資料儲存到資料庫中。

例如:

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

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

save() 方法可以插入或更新資料列,具體取決於 Active Record 實例的狀態。如果實例是透過 new 運算子新建立的,則呼叫 save() 將會導致插入新列;如果實例是查詢方法的結果,則呼叫 save() 將會更新與該實例關聯的資料列。

您可以透過檢查 Active Record 實例的 isNewRecord 屬性值來區分 Active Record 實例的兩種狀態。此屬性也會在內部由 save() 使用,如下所示:

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

提示:您可以直接呼叫 insert()update() 來插入或更新資料列。

資料驗證

由於 yii\db\ActiveRecord 擴展自 yii\base\Model,因此它共用相同的 資料驗證 功能。您可以透過覆寫 rules() 方法來宣告驗證規則,並透過呼叫 validate() 方法來執行資料驗證。

當您呼叫 save() 時,預設情況下它會自動呼叫 validate()。只有在驗證通過時,它才會實際儲存資料;否則它只會傳回 false,您可以檢查 errors 屬性來擷取驗證錯誤訊息。

提示:如果您確定您的資料不需要驗證(例如,資料來自可信任的來源),您可以呼叫 save(false) 來略過驗證。

大量賦值

與一般的 模型 類似,Active Record 實例也享有 大量賦值功能。使用此功能,您可以在單一 PHP 語句中為 Active Record 實例的多個屬性指派值,如下所示。但請記住,只有 安全屬性 才能大量賦值。

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

$customer = new Customer();

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

更新計數器

遞增或遞減資料庫表中的欄位是一項常見的任務。我們將這些欄位稱為「計數器欄位」。您可以使用 updateCounters() 來更新一個或多個計數器欄位。例如:

$post = Post::findOne(100);

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

注意:如果您使用 yii\db\ActiveRecord::save() 來更新計數器欄位,您最終可能會得到不準確的結果,因為很可能多個請求正在儲存相同的計數器,而這些請求會讀取和寫入相同的計數器值。

髒屬性

當您呼叫 save() 以儲存 Active Record 實例時,只會儲存髒屬性。如果屬性的值自從從資料庫載入或上次儲存到資料庫後被修改過,則該屬性被視為髒屬性。請注意,無論 Active Record 實例是否具有髒屬性,都會執行資料驗證。

Active Record 會自動維護髒屬性清單。它的做法是維護舊版的屬性值,並將其與最新的屬性值進行比較。您可以呼叫 yii\db\ActiveRecord::getDirtyAttributes() 以取得目前為髒屬性的屬性。您也可以呼叫 yii\db\ActiveRecord::markAttributeDirty() 來明確將屬性標記為髒屬性。

如果您對最近一次修改之前的屬性值感興趣,您可以呼叫 getOldAttributes()getOldAttribute()

注意:新舊值的比較將使用 === 運算子完成,因此即使值相同但類型不同,值也會被視為髒屬性。當模型從 HTML 表單接收使用者輸入時,通常會發生這種情況,其中每個值都表示為字串。為了確保正確的類型,例如整數值,您可以套用驗證篩選器['attributeName', 'filter', 'filter' => 'intval']。這適用於 PHP 的所有型別轉換函式,例如 intval()floatval()boolval 等...

預設屬性值

您的某些資料表欄位可能在資料庫中定義了預設值。有時,您可能會想要使用這些預設值預先填入 Active Record 實例的 Web 表單。為了避免再次寫入相同的預設值,您可以呼叫 loadDefaultValues(),將資料庫定義的預設值填入對應的 Active Record 屬性

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

屬性型別轉換

yii\db\ActiveRecord 在由查詢結果填入時,會使用來自 資料庫表結構描述 的資訊,對其屬性值執行自動型別轉換。這允許從宣告為整數的資料表欄位擷取的資料,以 PHP 整數填入 ActiveRecord 實例,布林值以布林值填入,依此類推。但是,型別轉換機制有一些限制:

  • 浮點數值不會轉換,並且會表示為字串,否則它們可能會失去精確度。
  • 整數值的轉換取決於您使用的作業系統的整數容量。特別是:宣告為「不帶正負號的整數」或「大整數」的欄位值只會在 64 位元作業系統上轉換為 PHP 整數,而在 32 位元作業系統上,它們將表示為字串。

請注意,屬性型別轉換僅在從查詢結果填入 ActiveRecord 實例期間執行。從 HTTP 請求載入或透過屬性存取直接設定的值,不會自動轉換。資料表結構描述也會在準備用於 ActiveRecord 資料儲存的 SQL 語句時使用,以確保值以正確的類型繫結到查詢。但是,ActiveRecord 實例屬性值在儲存過程中不會轉換。

提示:您可以使用 yii\behaviors\AttributeTypecastBehavior 來協助 ActiveRecord 驗證或儲存時的屬性值型別轉換。

自 2.0.14 版本起,Yii ActiveRecord 支援複雜的資料類型,例如 JSON 或多維陣列。

MySQL 與 PostgreSQL 中的 JSON

資料填入後,JSON 欄位中的值將會根據標準 JSON 解碼規則,從 JSON 自動解碼。

若要將屬性值儲存到 JSON 欄位,ActiveRecord 將會自動建立一個 JsonExpression 物件,該物件將會在 QueryBuilder 層級編碼為 JSON 字串。

PostgreSQL 中的陣列

資料填入後,Array 欄位中的值將會從 PgSQL 標記法自動解碼為 ArrayExpression 物件。它實作了 PHP ArrayAccess 介面,因此您可以將其用作陣列,或呼叫 ->getValue() 以取得陣列本身。

若要將屬性值儲存到陣列欄位,ActiveRecord 將會自動建立一個 ArrayExpression 物件,該物件將會由 QueryBuilder 編碼為陣列的 PgSQL 字串表示法。

您也可以使用 JSON 欄位的條件

$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])])

若要深入瞭解運算式建構系統,請閱讀 查詢產生器 – 新增自訂條件和運算式 文章。

更新多列

上述方法都適用於個別的 Active Record 實例,導致插入或更新個別的資料表列。若要同時更新多列,您應該改為呼叫 updateAll(),這是一個靜態方法。

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

同樣地,您可以呼叫 updateAllCounters(),同時更新多列的計數器欄位。

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

刪除資料

若要刪除單一資料列,請先擷取對應於該資料列的 Active Record 實例,然後呼叫 yii\db\ActiveRecord::delete() 方法。

$customer = Customer::findOne(123);
$customer->delete();

您可以呼叫 yii\db\ActiveRecord::deleteAll() 來刪除多個或所有資料列。例如:

Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);

注意:呼叫 deleteAll() 時務必非常小心,因為如果您在指定條件時出錯,它可能會完全清除資料表中的所有資料。

Active Record 生命週期

務必瞭解 Active Record 在用於不同目的時的生命週期。在每個生命週期中,都會調用特定的方法序列,您可以覆寫這些方法,以取得自訂生命週期的機會。您也可以回應在生命週期期間觸發的特定 Active Record 事件,以注入您的自訂程式碼。當您開發需要自訂 Active Record 生命週期的 Active Record 行為 時,這些事件特別有用。

在下文中,我們將總結各種 Active Record 生命週期,以及生命週期中涉及的方法/事件。

新實例生命週期

當透過 new 運算子建立新的 Active Record 實例時,將會發生以下生命週期:

  1. 類別建構函式。
  2. init():觸發 EVENT_INIT 事件。

查詢資料生命週期

當透過其中一種查詢方法查詢資料時,每個新填入的 Active Record 都將經歷以下生命週期:

  1. 類別建構函式。
  2. init():觸發 EVENT_INIT 事件。
  3. afterFind():觸發 EVENT_AFTER_FIND 事件。

儲存資料生命週期

當呼叫 save() 以插入或更新 Active Record 實例時,將會發生以下生命週期:

  1. beforeValidate():觸發 EVENT_BEFORE_VALIDATE 事件。如果方法傳回 falseyii\base\ModelEvent::$isValidfalse,則會略過其餘步驟。
  2. 執行資料驗證。如果資料驗證失敗,則會略過步驟 3 之後的步驟。
  3. afterValidate():觸發 EVENT_AFTER_VALIDATE 事件。
  4. beforeSave():觸發 EVENT_BEFORE_INSERTEVENT_BEFORE_UPDATE 事件。如果此方法回傳 falseyii\base\ModelEvent::$isValidfalse,剩餘步驟將會被跳過。
  5. 執行實際的資料插入或更新。
  6. afterSave():觸發 EVENT_AFTER_INSERTEVENT_AFTER_UPDATE 事件。

刪除資料生命週期

當呼叫 delete() 以刪除一個 Active Record 實例時,將會發生以下生命週期

  1. beforeDelete():觸發 EVENT_BEFORE_DELETE 事件。如果此方法回傳 falseyii\base\ModelEvent::$isValidfalse,剩餘步驟將會被跳過。
  2. 執行實際的資料刪除。
  3. afterDelete():觸發 EVENT_AFTER_DELETE 事件。

注意:呼叫以下任何方法將不會啟動上述任何生命週期,因為它們直接在資料庫上運作,而不是基於記錄的方式

注意:基於效能考量,預設情況下不支援 DI。如果需要支援,您可以覆寫 instantiate() 方法,透過 Yii::createObject() 來實例化類別以新增支援。

public static function instantiate($row)
{
    return Yii::createObject(static::class);
}

重新整理資料生命週期

當呼叫 refresh() 以重新整理 Active Record 實例時,如果重新整理成功且方法回傳 true,則會觸發 EVENT_AFTER_REFRESH 事件。

使用事務

在使用 Active Record 時,有兩種方式可以使用 事務

第一種方式是將 Active Record 方法呼叫顯式地封閉在事務區塊中,如下所示,

$customer = Customer::findOne(123);

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

// or alternatively

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

注意:在上述程式碼中,我們有兩個 catch 區塊,以相容於 PHP 5.x 和 PHP 7.x。\Exception 從 PHP 7.0 開始實作了 \Throwable 介面,因此如果您的應用程式僅使用 PHP 7.0 及更高版本,您可以跳過 \Exception 的部分。

第二種方式是在 yii\db\ActiveRecord::transactions() 方法中列出需要事務支援的資料庫操作。例如,

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // the above is equivalent to the following:
            // 'api' => self::OP_ALL,
        ];
    }
}

yii\db\ActiveRecord::transactions() 方法應回傳一個陣列,其鍵為 情境 名稱,值為應封閉在事務中的對應操作。您應使用以下常數來指稱不同的資料庫操作

使用 | 運算子串連上述常數以表示多個操作。您也可以使用快捷常數 OP_ALL 來指稱上述所有三個操作。

使用此方法建立的事務將在呼叫 beforeSave() 之前啟動,並在 afterSave() 執行後提交。

樂觀鎖定

樂觀鎖定是一種防止當單一資料列被多個使用者更新時可能發生的衝突的方法。例如,使用者 A 和使用者 B 同時編輯同一篇 wiki 文章。在使用者 A 儲存他的編輯後,使用者 B 點擊「儲存」按鈕,試圖也儲存他的編輯。由於使用者 B 實際上是在過時版本的文章上工作,因此最好有一種方法可以阻止他儲存文章,並向他顯示一些提示訊息。

樂觀鎖定透過使用一個欄位來記錄每個資料列的版本號碼來解決上述問題。當使用過時的版本號碼儲存資料列時,將會拋出 yii\db\StaleObjectException 例外,這會阻止資料列被儲存。只有當您使用 yii\db\ActiveRecord::update()yii\db\ActiveRecord::delete() 分別更新或刪除現有的資料列時,才支援樂觀鎖定。

要使用樂觀鎖定,

  1. 在與 Active Record 類別相關聯的資料庫表格中建立一個欄位,以儲存每個資料列的版本號碼。該欄位應為大整數類型(在 MySQL 中,它將是 BIGINT DEFAULT 0)。
  2. 覆寫 yii\db\ActiveRecord::optimisticLock() 方法以回傳此欄位的名稱。
  3. 在您的模型類別中實作 OptimisticLockBehavior,以自動從收到的請求中解析其值。從驗證規則中移除版本屬性,因為 OptimisticLockBehavior 應處理它。
  4. 在接收使用者輸入的 Web 表單中,新增一個隱藏欄位以儲存正在更新的資料列的目前版本號碼。
  5. 在控制器動作中使用 Active Record 更新資料列時,嘗試捕獲 yii\db\StaleObjectException 例外。實作必要的業務邏輯(例如,合併變更、提示過時資料)以解決衝突。

例如,假設版本欄位名為 version。您可以使用以下程式碼實作樂觀鎖定。

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

use yii\helpers\Html;

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


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

use yii\db\StaleObjectException;

public function actionUpdate($id)
{
    $model = $this->findModel($id);

    try {
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    } catch (StaleObjectException $e) {
        // logic to resolve the conflict
    }
}

// ------ model code -------

use yii\behaviors\OptimisticLockBehavior;

public function behaviors()
{
    return [
        OptimisticLockBehavior::class,
    ];
}

public function optimisticLock()
{
    return 'version';
}

注意:由於 OptimisticLockBehavior 將透過直接解析 getBodyParam() 來確保僅在使用者提交有效的版本號碼時才儲存記錄,因此擴充您的模型類別並在父模型中執行步驟 2,同時將行為(步驟 3)附加到子類別可能很有用,這樣您就可以擁有一個專用於內部使用的實例,同時將另一個實例綁定到負責接收終端使用者輸入的控制器。或者,您可以透過設定其 value 屬性來實作您自己的邏輯。

使用關聯式資料

除了使用個別的資料庫表格外,Active Record 也能夠將相關資料匯集在一起,使其可以透過主要資料輕鬆存取。例如,客戶資料與訂單資料相關,因為一個客戶可能下達一個或多個訂單。透過適當地宣告此關聯,您將能夠使用運算式 $customer->orders 來存取客戶的訂單資訊,這會以 Order Active Record 實例陣列的形式回傳客戶的訂單資訊。

宣告關聯

要使用 Active Record 處理關聯式資料,您首先需要在 Active Record 類別中宣告關聯。任務就像為每個感興趣的關聯宣告一個關聯方法一樣簡單,如下所示,

class Customer extends ActiveRecord
{
    // ...

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

class Order extends ActiveRecord
{
    // ...

    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

在上面的程式碼中,我們為 Customer 類別宣告了一個 orders 關聯,並為 Order 類別宣告了一個 customer 關聯。

每個關聯方法都必須命名為 getXyz。我們稱 xyz(第一個字母為小寫)為關聯名稱。請注意,關聯名稱是區分大小寫的。

在宣告關聯時,您應指定以下資訊

  • 關聯的多重性:透過呼叫 hasMany()hasOne() 來指定。在上面的範例中,您可以輕鬆地從關聯宣告中讀取到一個客戶有多個訂單,而一個訂單只有一個客戶。
  • 相關 Active Record 類別的名稱:指定為 hasMany()hasOne() 的第一個參數。建議的做法是呼叫 Xyz::class 以取得類別名稱字串,以便您可以獲得 IDE 自動完成支援以及編譯階段的錯誤偵測。
  • 兩種資料類型之間的連結:指定兩種資料類型透過哪些欄位關聯。陣列值是主要資料的欄位(由您正在宣告關聯的 Active Record 類別表示),而陣列鍵是相關資料的欄位。

    一個容易記住此規則的方法是,正如您在上面的範例中所看到的,您將屬於相關 Active Record 的欄位直接寫在其旁邊。您會看到 customer_idOrder 的屬性,而 idCustomer 的屬性。

警告:關聯名稱 relation 是保留字。使用時會產生 ArgumentCountError

存取關聯式資料

宣告關聯後,您可以透過關聯名稱存取關聯式資料。這就像存取由關聯方法定義的物件屬性一樣。因此,我們稱之為關聯屬性。例如,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders is an array of Order objects
$orders = $customer->orders;

資訊:當您透過 getter 方法 getXyz() 宣告名為 xyz 的關聯時,您將能夠像存取 物件屬性 一樣存取 xyz。請注意,名稱是區分大小寫的。

如果關聯是使用 hasMany() 宣告的,則存取此關聯屬性將回傳相關 Active Record 實例的陣列;如果關聯是使用 hasOne() 宣告的,則存取關聯屬性將回傳相關的 Active Record 實例,如果找不到相關資料,則回傳 null

當您第一次存取關聯屬性時,將會執行 SQL 語句,如上面的範例所示。如果再次存取相同的屬性,將會回傳先前的結果,而不會重新執行 SQL 語句。要強制重新執行 SQL 語句,您應先取消設定關聯屬性:unset($customer->orders)

注意:雖然此概念看起來類似於物件屬性功能,但有一個重要的區別。對於普通的物件屬性,屬性值的類型與定義 getter 方法的類型相同。然而,關聯方法回傳 yii\db\ActiveQuery 實例,而存取關聯屬性將回傳 yii\db\ActiveRecord 實例或這些實例的陣列。

$customer->orders; // is an array of `Order` objects
$customer->getOrders(); // returns an ActiveQuery instance

這對於建立自訂查詢很有用,這將在下一節中說明。

動態關聯查詢

由於關聯方法回傳 yii\db\ActiveQuery 的實例,因此您可以在執行資料庫查詢之前,使用查詢建構方法進一步建構此查詢。例如,

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

與存取關聯屬性不同,每次您透過關聯方法執行動態關聯查詢時,即使之前執行過相同的動態關聯查詢,也會執行 SQL 語句。

有時您甚至可能想要參數化關聯宣告,以便您可以更輕鬆地執行動態關聯查詢。例如,您可以如下宣告 bigOrders 關聯,

class Customer extends ActiveRecord
{
    public function getBigOrders($threshold = 100)
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])
            ->where('subtotal > :threshold', [':threshold' => $threshold])
            ->orderBy('id');
    }
}

然後您將能夠執行以下關聯查詢

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

透過聯結表格的關聯

在資料庫建模中,當兩個相關表格之間的多重性為多對多時,通常會引入聯結表格。例如,order 表格和 item 表格可以透過名為 order_item 的聯結表格相關聯。然後,一個訂單將對應多個訂單項目,而一個產品項目也將對應多個訂單項目。

在宣告此類關聯時,您會呼叫 via()viaTable() 來指定聯結表格。via()viaTable() 之間的區別在於,前者以現有關聯名稱指定聯結表格,而後者直接使用聯結表格。例如,

class Order extends ActiveRecord
{
    public function getItems()
    {
        return $this->hasMany(Item::class, ['id' => 'item_id'])
            ->viaTable('order_item', ['order_id' => 'id']);
    }
}

或替代方案,

class Order extends ActiveRecord
{
    public function getOrderItems()
    {
        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
    }

    public function getItems()
    {
        return $this->hasMany(Item::class, ['id' => 'item_id'])
            ->via('orderItems');
    }
}

使用使用聯結表格宣告的關聯與使用普通關聯相同。例如,

// SELECT * FROM `order` WHERE `id` = 100
$order = Order::findOne(100);

// SELECT * FROM `order_item` WHERE `order_id` = 100
// SELECT * FROM `item` WHERE `item_id` IN (...)
// returns an array of Item objects
$items = $order->items;

透過多個表格鏈接關聯定義

更進一步,可以透過使用 via() 鏈接關聯定義,透過多個表格定義關聯。考慮上面的範例,我們有類別 CustomerOrderItem。我們可以向 Customer 類別新增一個關聯,列出他們下達的所有訂單中的所有項目,並將其命名為 getPurchasedItems(),關聯的鏈接顯示在以下程式碼範例中

class Customer extends ActiveRecord
{
    // ...

    public function getPurchasedItems()
    {
        // customer's items, matching 'id' column of `Item` to 'item_id' in OrderItem
        return $this->hasMany(Item::class, ['id' => 'item_id'])
                    ->via('orderItems');
    }

    public function getOrderItems()
    {
        // customer's order items, matching 'id' column of `Order` to 'order_id' in OrderItem
        return $this->hasMany(OrderItem::class, ['order_id' => 'id'])
                    ->via('orders');
    }

    public function getOrders()
    {
        // same as above
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

延遲載入和預先載入

存取關聯式資料中,我們解釋說您可以像存取普通物件屬性一樣存取 Active Record 實例的關聯屬性。SQL 語句只會在您第一次存取關聯屬性時執行。我們稱這種關聯式資料存取方法為延遲載入。例如,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// no SQL executed
$orders2 = $customer->orders;

延遲載入使用起來非常方便。但是,當您需要存取多個 Active Record 實例的相同關聯屬性時,它可能會遇到效能問題。考慮以下程式碼範例。將執行多少個 SQL 語句?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

正如您從上面的程式碼註解中看到的那樣,正在執行 101 個 SQL 語句!這是因為每次您在 for 迴圈中存取不同 Customer 物件的 orders 關聯屬性時,都會執行 SQL 語句。

為了解決這個效能問題,您可以使用所謂的預先載入方法,如下所示,

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

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

透過呼叫 yii\db\ActiveQuery::with(),您指示 Active Record 在一個 SQL 語句中取回前 100 個客戶的訂單。因此,您將執行的 SQL 語句數量從 101 個減少到 2 個!

您可以預先載入一個或多個關聯。您甚至可以預先載入巢狀關聯。巢狀關聯是在相關的 Active Record 類別中宣告的關聯。例如,Customer 透過 orders 關聯與 Order 相關,而 Order 透過 items 關聯與 Item 相關。當查詢 Customer 時,您可以使用巢狀關聯表示法 orders.items 預先載入 items

以下程式碼顯示了 with() 的不同用法。我們假設 Customer 類別有兩個關聯 orderscountry,而 Order 類別有一個關聯 items

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

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

您可以預先載入深度巢狀關聯,例如 a.b.c.d。所有父關聯都將被預先載入。也就是說,當您使用 a.b.c.d 呼叫 with() 時,您將預先載入 aa.ba.b.ca.b.c.d

資訊:一般來說,當預先載入 N 個關聯時,其中 M 個關聯是使用聯結表格定義的,總共將執行 N+M+1 個 SQL 語句。請注意,巢狀關聯 a.b.c.d 算作 4 個關聯。

當預先載入關聯時,您可以使用匿名函式自訂相應的關聯查詢。例如,

// find customers and bring back together their country and active orders
// SELECT * FROM `customer`
// SELECT * FROM `country` WHERE `id` IN (...)
// SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
$customers = Customer::find()->with([
    'country',
    'orders' => function ($query) {
        $query->andWhere(['status' => Order::STATUS_ACTIVE]);
    },
])->all();

當自訂關聯的關聯查詢時,您應將關聯名稱指定為陣列鍵,並使用匿名函式作為相應的陣列值。匿名函式將接收一個 $query 參數,該參數表示用於執行關聯查詢的 yii\db\ActiveQuery 物件。在上面的程式碼範例中,我們透過附加關於訂單狀態的額外條件來修改關聯查詢。

注意:如果您在預先載入關聯時呼叫 select(),則必須確保已選取關聯宣告中引用的欄位。否則,相關模型可能無法正確載入。例如,

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer is always `null`. To fix the problem, you should do the following:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

使用關聯進行聯結

注意:本小節中描述的內容僅適用於關聯式資料庫,例如 MySQL、PostgreSQL 等。

到目前為止,我們描述的關聯查詢僅在查詢主要資料時參考主要表格欄位。實際上,我們經常需要參考相關表格中的欄位。例如,我們可能想要取回至少有一個活動訂單的客戶。為了解決這個問題,我們可以建構一個聯結查詢,如下所示

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

注意:在建構涉及 JOIN SQL 語句的關聯查詢時,務必消除欄位名稱的歧義。常見的做法是以其對應的表格名稱作為欄位名稱的前綴。

但是,更好的方法是透過呼叫 yii\db\ActiveQuery::joinWith() 來利用現有的關聯宣告

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

兩種方法都執行相同的 SQL 語句集。但是,後一種方法更簡潔且更簡練。

預設情況下,joinWith() 將使用 LEFT JOIN 將主要表格與相關表格聯結。您可以透過其第三個參數 $joinType 指定不同的聯結類型(例如 RIGHT JOIN)。如果您想要的聯結類型是 INNER JOIN,您可以直接呼叫 innerJoinWith() 來代替。

呼叫 joinWith() 預設會預先載入相關資料。如果您不想引入相關資料,您可以將其第二個參數 $eagerLoading 指定為 false

注意:即使在使用 joinWith()innerJoinWith() 並啟用預先載入的情況下,相關資料也不會JOIN 查詢的結果中填入。因此,對於每個聯結的關聯,仍然有一個額外的查詢,如預先載入章節中所述。

with() 類似,您可以與一個或多個關聯聯結;您可以即時自訂關聯查詢;您可以與巢狀關聯聯結;並且您可以混合使用 with()joinWith()。例如,

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

有時,當聯結兩個表格時,您可能需要在 JOIN 查詢的 ON 部分中指定一些額外條件。這可以透過呼叫 yii\db\ActiveQuery::onCondition() 方法來完成,如下所示

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->onCondition(['order.status' => Order::STATUS_ACTIVE]);
    },
])->all();

上面的查詢取回所有客戶,並且對於每個客戶,它取回所有活動訂單。請注意,這與我們之前的範例不同,之前的範例僅取回至少有一個活動訂單的客戶。

資訊:yii\db\ActiveQuery 透過 onCondition() 指定條件時,如果查詢涉及 JOIN 查詢,則該條件將放在 ON 部分中。如果查詢不涉及 JOIN,則 on-condition 將自動附加到查詢的 WHERE 部分。因此,它可能僅包含包含相關表格欄位的條件。

關聯表格別名

如前所述,在查詢中使用 JOIN 時,我們需要消除欄位名稱的歧義。因此,通常為表格定義別名。透過以下方式自訂關聯查詢,可以為關聯查詢設定別名

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

但是,這看起來非常複雜,並且涉及硬編碼相關物件表格名稱或呼叫 Order::tableName()。自 2.0.7 版起,Yii 為此提供了一個快捷方式。您現在可以定義和使用關聯表格的別名,如下所示

// join the orders relation and sort the result by orders.id
$query->joinWith(['orders o'])->orderBy('o.id');

上面的語法適用於簡單的關聯。如果您在聯結巢狀關聯(例如 $query->joinWith(['orders.product']))時需要中間表格的別名,則需要像以下範例中那樣巢狀 joinWith 呼叫

$query->joinWith(['orders o' => function($q) {
        $q->joinWith('product p');
    }])
    ->where('o.amount > 100');

反向關聯

關聯宣告在兩個 Active Record 類別之間通常是互惠的。例如,Customer 透過 orders 關聯與 Order 相關,而 Order 透過 customer 關聯與 Customer 反向相關。

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

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

現在考慮以下程式碼片段

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SELECT * FROM `customer` WHERE `id` = 123
$customer2 = $order->customer;

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

我們可能會認為 $customer$customer2 是相同的,但它們不是!實際上,它們確實包含相同的客戶資料,但它們是不同的物件。當存取 $order->customer 時,會執行額外的 SQL 語句來填入一個新的物件 $customer2

為了避免在上面的範例中冗餘執行最後一個 SQL 語句,我們應該透過呼叫 inverseOf() 方法來告訴 Yii,customerorders反向關聯,如下所示

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
    }
}

透過此修改後的關聯宣告,我們將擁有

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

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

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

注意:反向關聯不能為涉及聯結表格的關聯定義。也就是說,如果使用 via()viaTable() 定義關聯,則不應進一步呼叫 inverseOf()

儲存關聯

當使用關聯式資料時,您經常需要建立不同資料之間的關係或破壞現有的關係。這需要為定義關聯的欄位設定正確的值。使用 Active Record,您最終可能會編寫如下程式碼

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

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

Active Record 提供了 link() 方法,讓您可以更優雅地完成此任務

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

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

link() 方法要求您指定關聯名稱和應建立關係的目標 Active Record 實例。該方法將修改連結兩個 Active Record 實例的屬性值,並將其儲存到資料庫。在上面的範例中,它會將 Order 實例的 customer_id 屬性設定為 Customer 實例的 id 屬性的值,然後將其儲存到資料庫。

注意:您無法連結兩個新建立的 Active Record 實例。

當關聯是透過聯結表格定義時,使用 link() 的好處更加明顯。例如,您可以使用以下程式碼將 Order 實例與 Item 實例連結

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

上面的程式碼將自動在 order_item 聯結表格中插入一列,以將訂單與項目關聯起來。

資訊:link() 方法在儲存受影響的 Active Record 實例時不會執行任何資料驗證。您有責任在呼叫此方法之前驗證任何輸入資料。

link() 的相反操作是 unlink(),它會中斷兩個 Active Record 實例之間的現有關係。例如,

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

預設情況下,unlink() 方法會將指定現有關係的外鍵值設定為 null。但是,您可以選擇透過將 $delete 參數作為 true 傳遞給方法來刪除包含外鍵值的表格列。

當聯結表格涉及關聯時,呼叫 unlink() 將導致聯結表格中的外鍵被清除,或者如果 $deletetrue,則會刪除聯結表格中的相應列。

跨資料庫關聯

Active Record 允許您在由不同資料庫驅動的 Active Record 類別之間宣告關聯。資料庫可以是不同的類型(例如 MySQL 和 PostgreSQL,或 MS SQL 和 MongoDB),並且它們可以在不同的伺服器上執行。您可以使用相同的語法來執行關聯查詢。例如,

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

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

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

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

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

您可以使用本節中描述的大部分關聯查詢功能。

注意:joinWith() 的使用僅限於允許跨資料庫 JOIN 查詢的資料庫。因此,您無法在上面的範例中使用此方法,因為 MongoDB 不支援 JOIN。

自訂查詢類別

預設情況下,所有 Active Record 查詢都由 yii\db\ActiveQuery 支援。要在 Active Record 類別中使用自訂查詢類別,您應覆寫 yii\db\ActiveRecord::find() 方法並回傳您的自訂查詢類別的實例。例如,

// file Comment.php
namespace app\models;

use yii\db\ActiveRecord;

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

現在,每當您使用 Comment 執行查詢(例如 find()findOne())或定義關聯(例如 hasOne())時,您都將呼叫 CommentQuery 的實例,而不是 ActiveQuery

您現在必須定義 CommentQuery 類別,該類別可以透過許多創新的方式進行自訂,以改善您的查詢建構體驗。例如,

// file CommentQuery.php
namespace app\models;

use yii\db\ActiveQuery;

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

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

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

注意:在定義新的查詢建構方法時,通常應呼叫 andOnCondition()orOnCondition() 而不是呼叫 onCondition(),以附加額外的條件,以便不會覆寫任何現有條件。

這允許您編寫如下查詢建構程式碼

$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();

提示:在大型專案中,建議您使用自訂查詢類別來保存大多數與查詢相關的程式碼,以便可以保持 Active Record 類別的整潔。

您也可以在使用 Comment 定義關聯或執行關聯查詢時使用新的查詢建構方法

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

$customers = Customer::find()->joinWith('activeComments')->all();

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

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

資訊:在 Yii 1.1 中,有一個稱為範圍的概念。範圍在 Yii 2.0 中不再直接支援,您應使用自訂查詢類別和查詢方法來達到相同的目標。

選取額外欄位

當從查詢結果填入 Active Record 實例時,其屬性會由接收到的資料集中的相應欄位值填入。

您可以從查詢中提取額外的欄位或值,並將其儲存在 Active Record 中。例如,假設我們有一個名為 room 的表格,其中包含有關飯店可用房間的資訊。每個房間都使用欄位 lengthwidthheight 儲存其幾何尺寸的資訊。假設我們需要以遞減順序檢索所有可用房間及其體積的列表。因此您無法使用 PHP 計算體積,因為我們需要按其值對記錄進行排序,但您也希望在列表中顯示 volume。為了實現此目標,您需要在 Room Active Record 類別中宣告一個額外欄位,該欄位將儲存 volume

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

    // ...
}

然後您需要撰寫一個查詢,該查詢計算房間的體積並執行排序

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

foreach ($rooms as $room) {
    echo $room->volume; // contains value calculated by SQL
}

選取額外欄位的功能對於彙總查詢可能非常有用。假設您需要顯示客戶列表以及他們下的訂單數量。首先,您需要宣告一個具有 orders 關聯和額外欄位以儲存計數的 Customer 類別

class Customer extends \yii\db\ActiveRecord
{
    public $ordersCount;

    // ...

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

然後您可以撰寫一個查詢,該查詢聯結訂單並計算其計數

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // select all customer fields
        'COUNT({{order}}.id) AS ordersCount' // calculate orders count
    ])
    ->joinWith('orders') // ensure table junction
    ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works
    ->all();

使用此方法的缺點是,如果資訊未在 SQL 查詢中載入,則必須單獨計算。因此,如果您透過沒有額外 select 語句的常規查詢找到特定記錄,則它將無法回傳額外欄位的實際值。新儲存的記錄也會發生同樣的情況。

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

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

使用 __get()__set() 魔術方法,我們可以模擬屬性的行為

class Room extends \yii\db\ActiveRecord
{
    private $_volume;
    
    public function setVolume($volume)
    {
        $this->_volume = (float) $volume;
    }
    
    public function getVolume()
    {
        if (empty($this->length) || empty($this->width) || empty($this->height)) {
            return null;
        }
        
        if ($this->_volume === null) {
            $this->setVolume(
                $this->length * $this->width * $this->height
            );
        }
        
        return $this->_volume;
    }

    // ...
}

當 select 查詢未提供體積時,模型將能夠使用模型的屬性自動計算它。

您也可以使用定義的關聯性來計算彙總欄位。

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

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

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

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

        return $this->_ordersCount;
    }

    // ...

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

在此程式碼中,如果 'select' 語句中存在 'ordersCount',則 Customer::ordersCount 將會由查詢結果填充;否則,它將會根據需要使用 Customer::orders 關聯性來計算。

這種方法也可以用於為某些關聯式資料建立捷徑,特別是針對彙總。例如

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

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

    /**
     * Declares new relation based on 'orders', which provides aggregation.
     */
    public function getOrdersAggregation()
    {
        return $this->getOrders()
            ->select(['customer_id', 'counted' => 'count(*)'])
            ->groupBy('customer_id')
            ->asArray(true);
    }

    // ...
}

foreach (Customer::find()->with('ordersAggregation')->all() as $customer) {
    echo $customer->ordersCount; // outputs aggregation data from relation without extra query due to eager loading
}

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

發現錯字或您認為此頁面需要改進嗎?
在 github 上編輯 !