Active Record
雖然 Yii DAO 可以處理幾乎任何數據庫相關的任務, 但很可能我們會花費 90% 的時間以編寫一些執行普通 CRUD(create, read, update 和 delete)操作的 SQL 語句。 而且我們的代碼中混雜了SQL語句時也會變得難以維護。要解決這些問題,我們可以使用 Active Record。
Active Record (AR) 是一個流行的 對象-關係映射 (ORM) 技術。 每個 AR 類代表一個數據表(或視圖),數據表(或視圖)的列在 AR 類中體現爲類的屬性,一個 AR 實例則表示表中的一行。 常見的 CRUD 操作作爲 AR 的方法實現。因此,我們可以以一種更加面向對象的方式訪問數據。 例如,我們可以使用以下代碼向 tbl_post
表中插入一個新行。
$post=new Post; $post->title='sample post'; $post->content='post body content'; $post->save();
下面我們講解怎樣設置 AR 並通過它執行 CRUD 操作。我們將在下一節中展示怎樣使用 AR 處理數據庫關係。 爲簡單起見,我們使用下面的數據表作爲此節中的例子。注意,如果你使用 MySQL 數據庫,你應該將下面的 SQL 中的 AUTOINCREMENT
替換爲 AUTO_INCREMENT
。
CREATE TABLE tbl_post ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, title VARCHAR(128) NOT NULL, content TEXT NOT NULL, create_time INTEGER NOT NULL );
注意: AR 並非要解決所有數據庫相關的任務。它的最佳應用是模型化數據表爲 PHP 結構和執行不包含複雜 SQL 語句的查詢。 對於複雜查詢的場景,應使用 Yii DAO。
建立數據庫連接
AR 依靠一個數據庫連接以執行數據庫相關的操作。默認情況下, 它假定 db
應用組件提供了所需的 CDbConnection 數據庫連接實例。如下應用配置提供了一個例子:
return array( 'components'=>array( 'db'=>array( 'class'=>'system.db.CDbConnection', 'connectionString'=>'sqlite:path/to/dbfile', // 開啓表結構緩存(schema caching)提高性能 // 'schemaCachingDuration'=>3600, ), ), );
提示: 由於 Active Record 依靠表的元數據(metadata)測定列的信息,讀取元數據並解析需要時間。 如果你數據庫的表結構很少改動,你應該通過配置 CDbConnection::schemaCachingDuration 屬性的值爲一個大於零的值開啓表結構緩存。
對 AR 的支持受 DBMS 的限制,當前只支持下列幾種 DBMS:
注意: 1.0.4 版開始支持 Microsoft SQL Server;1.0.5 版開始支持 Oracle。
如果你想使用一個不是 db
的應用組件,或者如果你想使用
AR 處理多個數據庫,你應該覆蓋CActiveRecord::getDbConnection()。 CActiveRecord 類是所有
AR 類的基類。
提示: 通過 AR 使用多個數據庫有兩種方式。如果數據庫的結構不同,你可以創建不同的 AR 基類實現不同的getDbConnection()。否則,動態改變靜態變量 CActiveRecord::db 是一個好主意。
定義 AR 類
要訪問一個數據表,我們首先需要通過集成 CActiveRecord 定義一個
AR 類。 每個 AR 類代表一個單獨的數據表,一個 AR 實例則代表那個表中的一行。 如下例子演示了代表 tbl_post
表的
AR 類的最簡代碼:
class Post extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'tbl_post'; } }
提示: 由於 AR 類經常在多處被引用,我們可以導入包含 AR 類的整個目錄,而不是一個個導入。 例如,如果我們所有的 AR 類文件都在
protected/models
目錄中,我們可以配置應用如下:return array( 'import'=>array( 'application.models.*', ), );
默認情況下,AR 類的名字和數據表的名字相同。如果不同,請覆蓋 tableName() 方法。 model() 方法爲每個 AR 類聲明爲如此(稍後解釋)。
信息: 要使用 1.1.0 版本中引入的 表前綴功能 AR 類的 tableName() 方法可以通過如下方式覆蓋
public function tableName() { return '{{post}}'; }這就是說,我們將返回通過雙大括號括起來的沒有前綴的表名,而不是完整的表的名字。
數據錶行中列的值可以作爲相應 AR 實例的屬性訪問。例如,如下代碼設置了 title
列
(屬性):
$post=new Post; $post->title='a sample post';
雖然我們從未在 Post
類中顯式定義屬性 title
,我們還是可以通過上述代碼訪問。
這是因爲 title
是 tbl_post
表中的一個列,CActiveRecord
通過PHP的 __get()
魔術方法使其成爲一個可訪問的屬性。
如果我們嘗試以同樣的方式訪問一個不存在的列,將會拋出一個異常。
信息: 此指南中,我們在表名和列名中均使用了小寫字母。 這是因爲不同的 DBMS 處理大小寫的方式不同。 例如,PostgreSQL 默認情況下對列的名字大小寫不敏感,而且我們必須在一個查詢條件中用引號將大小寫混合的列名引起來。 使用小寫字母可以幫助我們避免此問題。
AR 依靠表中良好定義的主鍵。如果一個表沒有主鍵,則必須在相應的 AR 類中通過如下方式覆蓋 primaryKey()
方法指定哪一列或哪幾列作爲主鍵。
public function primaryKey() { return 'id'; // 對於複合主鍵,要返回一個類似如下的數組 // return array('pk1', 'pk2'); }
創建記錄
要向數據表中插入新行,我們要創建一個相應 AR 類的實例,設置其與表的列相關的屬性,然後調用 save() 方法完成插入:
$post=new Post; $post->title='sample post'; $post->content='content for the sample post'; $post->create_time=time(); $post->save();
如果表的主鍵是自增的,在插入完成後,AR 實例將包含一個更新的主鍵。在上面的例子中, id
屬性將反映出新插入帖子的主鍵值,即使我們從未顯式地改變它。
如果一個列在表結構中使用了靜態默認值(例如一個字符串,一個數字)定義。則 AR 實例中相應的屬性將在此實例創建時自動含有此默認值。改變此默認值的一個方式就是在 AR 類中顯示定義此屬性:
class Post extends CActiveRecord { public $title='please enter a title'; ...... } $post=new Post; echo $post->title; // 這兒將顯示: please enter a title
從版本 1.0.2 起,記錄在保存(插入或更新)到數據庫之前,其屬性可以賦值爲 CDbExpression 類型。
例如,爲保存一個由 MySQL 的 NOW()
函數返回的時間戳,我們可以使用如下代碼:
$post=new Post; $post->create_time=new CDbExpression('NOW()'); // $post->create_time='NOW()'; 不會起作用,因爲 // 'NOW()' 將會被作爲一個字符串處理。 $post->save();
提示: 由於 AR 允許我們無需寫一大堆 SQL 語句就能執行數據庫操作, 我們經常會想知道 AR 在背後到底執行了什麼 SQL 語句。這可以通過開啓 Yii 的 日誌功能 實現。例如,我們在應用配置中開啓了 CWebLogRoute ,我們將會在每個網頁的最後看到執行過的 SQL 語句。 從 1.0.5 版本起,我們可以在應用配置中設置CDbConnection::enableParamLogging 爲 true ,這樣綁定在 SQL 語句中的參數值也會被記錄。
讀取記錄
要讀取數據表中的數據,我們可以通過如下方式調用 find
系列方法中的一種:
// 查找滿足指定條件的結果中的第一行 $post=Post::model()->find($condition,$params); // 查找具有指定主鍵值的那一行 $post=Post::model()->findByPk($postID,$condition,$params); // 查找具有指定屬性值的行 $post=Post::model()->findByAttributes($attributes,$condition,$params); // 通過指定的 SQL 語句查找結果中的第一行 $post=Post::model()->findBySql($sql,$params);
如上所示,我們通過 Post::model()
調用 find
方法。
請記住,靜態方法 model()
是每個
AR 類所必須的。 此方法返回在對象上下文中的一個用於訪問類級別方法(類似於靜態類方法的東西)的 AR 實例。
如果 find
方法找到了一個滿足查詢條件的行,它將返回一個 Post
實例,實例的屬性含有數據錶行中相應列的值。
然後我們就可以像讀取普通對象的屬性那樣讀取載入的值,例如 echo
$post->title;
。
如果使用給定的查詢條件在數據庫中沒有找到任何東西, find
方法將返回
null 。
調用 find
時,我們使用 $condition
和 $params
指定查詢條件。此處 $condition
可以是
SQL 語句中的 WHERE
字符串,$params
則是一個參數數組,其中的值應綁定到 $condation
中的佔位符。例如:
// 查找 postID=10 的那一行 $post=Post::model()->find('postID=:postID', array(':postID'=>10));
注意: 在上面的例子中,我們可能需要在特定的 DBMS 中將
postID
列的引用進行轉義。 例如,如果我們使用 PostgreSQL,我們必須將此表達式寫爲"postID"=:postID
,因爲 PostgreSQL 在默認情況下對列名大小寫不敏感。
我們也可以使用 $condition
指定更復雜的查詢條件。
不使用字符串,我們可以讓 $condition
成爲一個 CDbCriteria 的實例,它允許我們指定不限於 WHERE
的條件。
例如:
$criteria=new CDbCriteria; $criteria->select='title'; // 只選擇 'title' 列 $criteria->condition='postID=:postID'; $criteria->params=array(':postID'=>10); $post=Post::model()->find($criteria); // $params 不需要了
注意,當使用 CDbCriteria 作爲查詢條件時,$params
參數不再需要了,因爲它可以在 CDbCriteria 中指定,就像上面那樣。
一種替代 CDbCriteria 的方法是給 find
方法傳遞一個數組。
數組的鍵和值各自對應標準(criterion)的屬性名和值,上面的例子可以重寫爲如下:
$post=Post::model()->find(array( 'select'=>'title', 'condition'=>'postID=:postID', 'params'=>array(':postID'=>10), ));
信息: 當一個查詢條件是關於按指定的值匹配幾個列時,我們可以使用 findByAttributes()。我們使
$attributes
參數是一個以列名做索引的值的數組。在一些框架中,此任務可以通過調用類似findByNameAndTitle
的方法實現。雖然此方法看起來很誘人, 但它常常引起混淆,衝突和比如列名大小寫敏感的問題。
當有多行數據匹配指定的查詢條件時,我們可以通過下面的 findAll
方法將他們全部帶回。
每個都有其各自的 find
方法,就像我們已經講過的那樣。
// 查找滿足指定條件的所有行 $posts=Post::model()->findAll($condition,$params); // 查找帶有指定主鍵的所有行 $posts=Post::model()->findAllByPk($postIDs,$condition,$params); // 查找帶有指定屬性值的所有行 $posts=Post::model()->findAllByAttributes($attributes,$condition,$params); // 通過指定的SQL語句查找所有行 $posts=Post::model()->findAllBySql($sql,$params);
如果沒有任何東西符合查詢條件,findAll
將返回一個空數組。這跟 find
不同,find
會在沒有找到什麼東西時返回
null。
除了上面講述的 find
和 findAll
方法,爲了方便,(Yii)還提供瞭如下方法:
// 獲取滿足指定條件的行數 $n=Post::model()->count($condition,$params); // 通過指定的 SQL 獲取結果行數 $n=Post::model()->countBySql($sql,$params); // 檢查是否至少有一行復合指定的條件 $exists=Post::model()->exists($condition,$params);
更新記錄
在 AR 實例填充了列的值之後,我們可以改變它們並把它們存回數據表。
$post=Post::model()->findByPk(10); $post->title='new post title'; $post->save(); // 將更改保存到數據庫
正如我們可以看到的,我們使用同樣的 save() 方法執行插入和更新操作。
如果一個 AR 實例是使用 new
操作符創建的,調用save() 將會向數據表中插入一行新數據;
如果 AR 實例是某個 find
或 findAll
方法的結果,調用 save() 將更新表中現有的行。
實際上,我們是使用 CActiveRecord::isNewRecord 說明一個
AR 實例是不是新的。
直接更新數據表中的一行或多行而不首先載入也是可行的。 AR 提供瞭如下方便的類級別方法實現此目的:
// 更新符合指定條件的行 Post::model()->updateAll($attributes,$condition,$params); // 更新符合指定條件和主鍵的行 Post::model()->updateByPk($pk,$attributes,$condition,$params); // 更新滿足指定條件的行的計數列 Post::model()->updateCounters($counters,$condition,$params);
在上面的代碼中, $attributes
是一個含有以
列名作索引的列值的數組; $counters
是一個由列名索引的可增加的值的數組;$condition
和 $params
在前面的段落中已有描述。
刪除記錄
如果一個 AR 實例被一行數據填充,我們也可以刪除此行數據。
$post=Post::model()->findByPk(10); // 假設有一個帖子,其 ID 爲 10 $post->delete(); // 從數據表中刪除此行
注意,刪除之後, AR 實例仍然不變,但數據表中相應的行已經沒了。
使用下面的類級別代碼,可以無需首先加載行就可以刪除它。
// 刪除符合指定條件的行 Post::model()->deleteAll($condition,$params); // 刪除符合指定條件和主鍵的行 Post::model()->deleteByPk($pk,$condition,$params);
數據驗證
當插入或更新一行時,我們常常需要檢查列的值是否符合相應的規則。 如果列的值是由最終用戶提供的,這一點就更加重要。總體來說,我們永遠不能相信任何來自客戶端的數據。
當調用 save() 時, AR 會自動執行數據驗證。 驗證是基於在 AR 類的 rules() 方法中指定的規則進行的。 關於驗證規則的更多詳情,請參考 聲明驗證規則 一節。 下面是保存記錄時所需的典型的工作流。
if($post->save()) { // 數據有效且成功插入/更新 } else { // 數據無效,調用 getErrors() 提取錯誤信息 }
當要插入或更新的數據由最終用戶在一個 HTML 表單中提交時,我們需要將其賦給相應的 AR 屬性。 我們可以通過類似如下的方式實現:
$post->title=$_POST['title']; $post->content=$_POST['content']; $post->save();
如果有很多列,我們可以看到一個用於這種複製的很長的列表。 這可以通過使用如下所示的 attributes 屬性簡化操作。 更多信息可以在 安全的特性賦值 一節和 創建動作 一節找到。
// 假設 $_POST['Post'] 是一個以列名索引列值爲值的數組 $post->attributes=$_POST['Post']; $post->save();
對比記錄
類似於表記錄,AR 實例由其主鍵值來識別。 因此,要對比兩個 AR 實例,假設它們屬於相同的 AR 類, 我們只需要對比它們的主鍵值。 然而,一個更簡單的方式是調用 CActiveRecord::equals()。
信息: 不同於 AR 在其他框架的執行, Yii 在其 AR 中支持多個主鍵. 一個複合主鍵由兩個或更多字段構成。相應地, 主鍵值在 Yii 中表現爲一個數組. primaryKey 屬性給出了一個 AR 實例的主鍵值。
自定義
CActiveRecord 提供了幾個佔位符方法,它們可以在子類中被覆蓋以自定義其工作流。
- beforeValidate 和
-
beforeSave 和 afterSave: 這兩個將在保存 AR 實例之前和之後被調用。
-
beforeDelete 和 afterDelete: 這兩個將在一個 AR 實例被刪除之前和之後被調用。
-
afterConstruct: 這個將在每個使用
new
操作符創建 AR 實例後被調用。 -
beforeFind: 這個將在一個 AR 查找器被用於執行查詢(例如
find()
,findAll()
)之前被調用。 1.0.9 版本開始可用。 -
afterFind: 這個將在每個 AR 實例作爲一個查詢結果創建時被調用。
使用 AR 處理事務
每個 AR 實例都含有一個屬性名叫 dbConnection ,是一個 CDbConnection 的實例,這樣我們可以在需要時配合 AR 使用由 Yii DAO 提供的 事務 功能:
$model=Post::model(); $transaction=$model->dbConnection->beginTransaction(); try { // 查找和保存是可能由另一個請求干預的兩個步驟 // 這樣我們使用一個事務以確保其一致性和完整性 $post=$model->findByPk(10); $post->title='new post title'; $post->save(); $transaction->commit(); } catch(Exception $e) { $transaction->rollBack(); }
命名範圍
Note: 對命名範圍的支持從版本 1.0.5 開始。 命名範圍的最初想法來源於 Ruby on Rails.
命名範圍(named scope) 表示一個 命名的(named) 查詢規則,它可以和其他命名範圍聯合使用並應用於 Active Record 查詢。
命名範圍主要是在 CActiveRecord::scopes() 方法中以名字-規則對的方式聲明。
如下代碼在 Post
模型類中聲明瞭兩個命名範圍, published
和 recently
。
class Post extends CActiveRecord { ...... public function scopes() { return array( 'published'=>array( 'condition'=>'status=1', ), 'recently'=>array( 'order'=>'create_time DESC', 'limit'=>5, ), ); } }
每個命名範圍聲明爲一個可用於初始化 CDbCriteria 實例的數組。
例如,recently
命名範圍指定 order
屬性爲create_time
DESC
, limit
屬性爲
5。他們翻譯爲查詢規則後就會返回最近的5篇帖子。
命名範圍多用作 find
方法調用的修改器。
幾個命名範圍可以鏈到一起形成一個更有約束性的查詢結果集。例如, 要找到最近發佈的帖子, 我們可以使用如下代碼:
$posts=Post::model()->published()->recently()->findAll();
總體來說,命名範圍必須出現在一個 find
方法調用的左邊。
它們中的每一個都提供一個查詢規則,並聯合到其他規則, 包括傳遞給 find
方法調用的那一個。
最終結果就像給一個查詢添加了一系列過濾器。
從版本 1.0.6 開始,命名範圍也可用於 update
和 delete
方法。
例如,如下代碼將刪除所有最近發佈的帖子:
Post::model()->published()->recently()->delete();
注意: 命名範圍只能用於類級別方法。也就是說,此方法必須使用
ClassName::model()
調用。
參數化的命名範圍
命名範圍可以參數化。例如, 我們想自定義 recently
命名範圍中指定的帖子數量,要實現此目的,不是在CActiveRecord::scopes 方法中聲明命名範圍,
而是需要定義一個名字和此命名範圍的名字相同的方法:
public function recently($limit=5) { $this->getDbCriteria()->mergeWith(array( 'order'=>'create_time DESC', 'limit'=>$limit, )); return $this; }
然後,我們就可以使用如下語句獲取3條最近發佈的帖子。
$posts=Post::model()->published()->recently(3)->findAll();
上面的代碼中,如果我們沒有提供參數 3,我們將默認獲取 5 條最近發佈的帖子。
默認的命名範圍
模型類可以有一個默認命名範圍,它將應用於所有 (包括相關的那些) 關於此模型的查詢。例如,一個支持多種語言的網站可能只想顯示當前用戶所指定的語言的內容。 因爲可能會有很多關於此網站內容的查詢, 我們可以定義一個默認的命名範圍以解決此問題。 爲實現此目的,我們覆蓋 CActiveRecord::defaultScope 方法如下:
class Content extends CActiveRecord { public function defaultScope() { return array( 'condition'=>"language='".Yii::app()->language."'", ); } }
現在,如果下面的方法被調用,將會自動使用上面定義的查詢規則:
$contents=Content::model()->findAll();
注意,默認的命名範圍只會應用於 SELECT
查詢。INSERT
, UPDATE
和 DELETE
查詢將被忽略。