雖然Yii的DAO可以完成所有的數據庫操作,我們還是要花90%的時間來寫一些CRUD的操作。如果這些操作跟SQL語句混合使用,將會變得非常難以維護。爲了解決這些困難,我們可以使用記錄集Active Record。
AR是一個流行的對象關係集的技術。每個AR類代表一個數據庫的表(視圖),這些表的字段就對應AR的屬性,一個AR的實例,就代表數據表中的記錄。常用的CRUD操作,一般都採用AR來實現,所以,我們可以用一種更加面向對象的方法來獲取數據。例如,我們可以用以下的代碼來實現一個記錄的插入:
- $post=new Post;
- $post->title='sample post';
- $post->content='post body content';
- $post->save();
接下來我們將介紹如何創建AR,並用他來執行CRUD操作。
下一節中,我們會爲大家展示如何用AR處理關係數據庫。現在先簡單的說,我們用以下的表作爲我們的演示使用。
- CREATE TABLE tbl post (
- id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
- title VARCHAR(128) NOT NULL,
- content TEXT NOT NULL,
- create time INTEGER NOT NULL
- );
注意:AR並不能用來完全解決數據庫相關的操作,他最好是用來作爲數據庫表的模型類,用來做CRUD的簡單操作,而不死執行復雜的SQL語句。如果是需要複雜的SQL,那麼用DAO來操作。
1.1 建立數據庫連接
AR是基於數據庫連接來進行數據庫相關的操作的。默認情況下,我們假設db是這個應用的數據庫連接組件。下面展示一個配置文件作爲例子
- return array(
- 'components'=>array(
- 'db'=>array(
- 'class'=>'system.db.CDbConnection',
- 'connectionString'=>'sqlite:path/to/dbfile',
- // turn on schema caching to improve performance
- // 'schemaCachingDuration'=>3600,
- ),
- ),
- );
上例中,如果是數據庫結構不經常改變,則啓用'schemaCachingDuration' 會使得數據庫更高效。
支持AR的數據庫系統有:(注意,不是所有數據庫都支持啊)
● MySQL 4.1 or later
● PostgreSQL 7.3 or later
● SQLite 2 and 3
● Microsoft SQL Server 2000 or later
● Oracle
如果你不想用db作爲數據庫句柄,或者是要連接多個數據庫,那麼需要重寫CActiveRecord::getDbConnection()。CActiveRecord類是所有AR類的基類。
Tips:有兩個方法讓AR操作多個數據庫。如果這些數據庫的架構是不一樣的(我的理解是不同的數據庫系統),那麼就必須去建立一個AR的基類,實現不同的getDbConnection()方法;而如果都是相同的數據庫系統,動態的去改變靜態變量CActiveRecord::db就是最好的方法了。
1.2 定義AR類
要訪問一個數據庫的表,我們首先要定義一個類,這個類繼承於CActiveRecort。每個AR類代表一個數據庫的表,一個AR對象,則代表數據表中的一條記錄。以下代碼最簡潔的展示瞭如何讓一個AR代表tbl_post表:
- class Post extends CActiveRecord
- {
- public static function model($className= CLASS )
- {
- return parent::model($className);
- }
- public function tableName()
- {
- return 'tbl_post';
- }
- }
提示:因爲AR類經常會被很多地方用到,所以,我們可以一次引入整個目錄。例如,我們所有的AR都是放在protected/models,我們在配置工程的時候就可以:
- return array(
- 'import'=>array(
- 'application.models.*',
- ),
- );
默認情況下,AR類的名稱就是數據庫中的表明。如果不一樣的話,就重載tableName()。
TIPS:如果使用了表前綴,那麼上例中的tableName()就應該按照下列的實現:
- public function tableName()
- {
- return 'ffpostgg';
- }
數據表中的字段值,可以通過AR的實例屬性來訪問,例如:
- $post=new Post;
- $post->title='a sample post';
雖然我們從來沒有顯示的在post中聲明title這個屬性,我們還是可以使用。這是因爲title是表tbl_post的一個字段,CActiveRecord通過PHP的_get()這個神奇的方法,讓他變成了AR的屬性。如果我們嘗試訪問一個不存在的字段時,會拋出異常。
AR是基於一個表的好的主鍵設計。如果表中不存在主鍵,那麼就要求我們重載primaryKey()這個辦法,來指定一個主鍵。
- public function primaryKey()
- {
- return 'id';
- // For composite primary key, return an array like the following
- // return array('pk1', 'pk2');
- }
1.3 創建記錄
要想在中插入一行記錄,我們就要新建一個AR的對象。設置了相對應字段的屬性值之後,調用save()的方法,就插入到數據庫中了。
- $post=new Post;
- $post->title='sample post';
- $post->content='content for the sample post';
- $post->create time=time();
- $post->save();
如果主鍵是自動增長的,那麼在保存之後,會自動加上該主鍵,並且數值自動增加。上例中,id的屬性就會是新的記錄集的ID,雖然我們改變他。
如果在表中的某些字段,已經有默認值了,那麼在插入時,AR的這些屬性就會自動賦予默認值。如果想要更改這些默認值,就需要顯示的在代碼中設置AR的屬性。
在記錄被保存到數據庫之前,屬性可以被設置爲CDbExpression類型的值。例如,爲了保存Mysql返回的now()值,我們可以用以下的代碼實現:
- $post=new Post;
- $post->create_time=new CDbExpression('NOW()');
- // $post->create_time='NOW()'; will not work because
- // 'NOW()' will be treated as a string
- $post->save();
TIPS:AR雖然讓我們可以不用寫整片的SQL語句,但是有時候還是想看看數據庫到底執行了什麼語句。我們可以在Yii的logging feature中開啓。例如,我們在項目工程配置中,開啓CWebLogRoute,我們就可以在網頁端看到所執行的SQL。我們也可以設置CDbConnection::enableParamLogging,這樣還可以看到具體的參數值。
1.4 讀取記錄
要讀取數據表的數據,我們調用以下的其中一種:
- // find the first row satisfying the specified condition
- $post=Post::model()->find($condition,$params);
- // find the row with the specified primary key
- $post=Post::model()->findByPk($postID,$condition,$params);
- // find the row with the specified attribute values
- $post=Post::model()->findByAttributes($attributes,$condition,$params);
- // find the first row using the specified SQL statement
- $post=Post::model()->findBySql($sql,$params);
上例中,我們調用Post::model()的find方法,注意,對每個AR類來說,一個靜態的model方法是必須的。這個靜態方法,返回一個AR實例,用來訪問類級別的方法。
如果find方法找到了匹配條件的記錄,就返回一個Post的實例。然後我們就可以像訪問我們普通類的屬性那樣訪問數據庫的值了,例如$post->title。
如果沒有找到匹配的記錄,find會返回一個空值。
調用find方法的時候,我們用$condition 和$params作爲查詢的條件。例如:
- // find the row with postID=10
- $post=Post::model()->find('postID=:postID', array(':postID'=>10));
我們也可以用$condition 作複雜的查詢。當然了,複雜的就不是一句話了,我們用CDbCriteria的實例作爲$condition ,然後我們就可以設定更多的條件了:
- $criteria=new CDbCriteria;
- $criteria->select='title'; // only select the 'title' column
- $criteria->condition='postID=:postID';
- $criteria->params=array(':postID'=>10);
- $post=Post::model()->find($criteria); // $params is not needed
我們用CDbCriteria作爲查詢條件以後,$params就不需要了。
還有一個替換CDbCriteria的方法是像find提供一個數組參數,這個數組參數中包含查詢條件以及對應的值。放例子,不解釋
- $post=Post::model()->find(array(
- 'select'=>'title',
- 'condition'=>'postID=:postID',
- 'params'=>array(':postID'=>10),
- ));
TIPS:如果查詢條件只是某字段的值等於特定的值,可以使用 findByAttributes()
如果符合查詢條件的記錄很多,我們可以通過調用findAll這些方法一次性全部讀取。
- // find all rows satisfying the specified condition
- $posts=Post::model()->findAll($condition,$params);
- // find all rows with the specified primary keys
- $posts=Post::model()->findAllByPk($postIDs,$condition,$params);
- // find all rows with the specified attribute values
- $posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
- // find all rows using the specified SQL statement
- $posts=Post::model()->findAllBySql($sql,$params);
如果沒有符合查詢條件的記錄,那麼以上方法就會放回一個空的數組。這跟find不同的是,find會返回NULL。
除了之前提到的find以及findAll這些方法,以下的方法也是很好用的:
- // get the number of rows satisfying the specified condition
- $n=Post::model()->count($condition,$params);
- // get the number of rows using the specified SQL statement
- $n=Post::model()->countBySql($sql,$params);
- // check if there is at least a row satisfying the specified condition
- $exists=Post::model()->exists($condition,$params);
1.5 更新記錄
當AR實例的屬性被字段值賦值後,我們可以更改這些屬性,可以將這些更新過的屬性值保存到數據庫中。
- $post=Post::model()->findByPk(10);
- $post->title='new post title';
- $post->save(); // save the change to database
正如我們看到的,不論是更新還是插入新數據,我們都是調用save這個方法。如果AR的對象是new出來的,調用save就是執行插入操作;如果對象是從find或者是findAll得到的,調用save的時候就是更新操作。事實上,我們可以調用CActiveRecord::isNewRecord來判斷AR對象是新的與否。
我們也可以在沒有裝載數據的情況下,更新一條或者是多條記錄。AR提供了下列便捷的類方法:
- // update the rows matching the specified condition
- Post::model()->updateAll($attributes,$condition,$params);
- // update the rows matching the specified condition and primary key(s)
- Post::model()->updateByPk($pk,$attributes,$condition,$params);
- // update counter columns in the rows satisfying the specified conditions
- Post::model()->updateCounters($counters,$condition,$params);
$attributes 是一組字段名;
condition 是更新的條件;
$params 參數值;
1.6 刪除數據
我們可以用下面的方法來刪除數據庫記錄
- $post=Post::model()->findByPk(10); // assuming there is a post whose ID is 10
- $post->delete(); // delete the row from the database table
注意!!!刪除之後,AR的屬性什麼的都沒變,但是數據庫記錄沒啦!!!
下面的這些方法可以不需要加載數據而直接刪除:
- // delete the rows matching the specified condition
- Post::model()->deleteAll($condition,$params);
- // delete the rows matching the specified condition and primary key(s)
- Post::model()->deleteByPk($pk,$condition,$params);
1.7 數據驗證
在更新或者是插入數據的時候,經常需要根據一些規則來驗證字段值師傅符合規範。當這些數據是由終端用戶提供的時候,這種校驗尤爲重要。正常情況下,我們是完全不信任任何用戶輸入信息的。
AR在用戶提交save之後,會自動校驗數據。校驗規則是AR裏的rules()方法設定的。至於具體的規則設定,參考之前就行了。以下是一個典型的工作流:
- if($post->save())
- {
- // data is valid and is successfully inserted/updated
- }
- else
- {
- // data is invalid. call getErrors() to retrieve error messages
- }
當更新或者是插入的值,是從終端用戶輸入的,那麼我們要給AR的屬性賦值:
- $post->title=$ POST['title'];
- $post->content=$ POST['content'];
- $post->save();
如果是多個字段一起更新,那麼可以採用之前提到的集體賦值:
- // assume $ POST['Post'] is an array of column values indexed by column names
- $post->attributes=$ POST['Post'];
- $post->save();
1.8 數據對比
就像表記錄一樣,AR的對象也跟主鍵一樣的,有唯一ID。所以,如果比較兩個AR對象,如果他們屬於同一個AR類,我們僅僅比較他們的主鍵。一個簡單的方法是調用CActiveRecord::equals()。 但是:
TIPS:跟其他框架不同,Yii支持聯合主鍵。聯合組建在Yii裏面是一個數組的形式。
1.9 自定義
AR類提供了一些虛函數,用來給子類重載,自定義工作流:
● beforeValidate and afterValidate: 在驗證之前(之後)調用
● beforeSave and afterSave:
● beforeDelete and afterDelete:
● afterConstruct:
● beforeFind:
● afterFind:
1.10 AR中使用事務
每個AR對象都包含了一個叫做dbConnection的屬性,也就是CDbConnection的句柄。所以在我們用AR的時候,可以使用DAO的事務功能。
- $model=Post::model();
- $transaction=$model->dbConnection->beginTransaction();
- try
- {
- // find and save are two steps which may be intervened by another request
- // we therefore use a transaction to ensure consistency and integrity
- $post=$model->findByPk(10);
- $post->title='new post title';
- $post->save();
- $transaction->commit();
- }
- catch(Exception $e)
- {
- $transaction->rollBack();
- }
1.11 命名空間
一個命名空間意味着一個命名的查詢條件(query criteria),可以用來連接其他的命名空間,運用於AR查詢。
命名空間主要是在CActiveRecord::scopes()裏聲明,格式爲name-criteria。下面的代碼定義了兩個命名空間: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的方法,每個命名空間都提供一個查詢規則。作用就好比在查詢語句中添加一串過濾器。
參數化命名空間
命名空間也可以參數化。例如,我們想定製recently這個命名空間裏的文章數量。我們不在CActiveRecord::scopes裏修改方法,換一種做法,我們要定義一個與這個命名空間同名的方法
- public function recently($limit=5)
- {
- $this->getDbCriteria()->mergeWith(array(
- 'order'=>'create time DESC',
- 'limit'=>$limit,
- ));
- return $this;
- }
然後我們就可以通過以下的方法來獲取最近的3篇文章(而不是5篇)
- $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 and DELETE。同樣的,當聲明一個空間(默認或者命名的),AR類不能用於當前的數據庫操作。