Yii快速入門(三)數據庫操作

Yii提供了強大的數據庫編程支持。Yii數據訪問對象(DAO)建立在PHP的數據對象(PDO)extension上,使得在一個單一的統一的接口可以訪問不同的數據庫管理系統(DBMS)。使用Yii的DAO開發的應用程序可以很容易地切換使用不同的數據庫管理系統,而不需要修改數據訪問代碼。Yii 的Active Record( AR ),實現了被廣泛采用的對象關系映射(ORM)辦法,進一步簡化數據庫編程。按照約定,一個類代表一個表,一個實例代表一行數據。Yii AR消除了大部分用于處理CRUD(創建,讀取,更新和刪除)數據操作的sql語句的重復任務。
盡管Yii的DAO和AR能夠處理幾乎所有數據庫相關的任務,您仍然可以在Yii application中使用自己的數據庫。事實上,Yii框架精心設計使得可以與其他第三方庫同時使用。

一、數據訪問對象 (DAO)

Yii DAO 基于 PHP Data Objects (PDO) 構建。它是一個為眾多流行的DBMS提供統一數據訪問的擴展,這些 DBMS 包括 MySQL, PostgreSQL 等等。因此,要使用 Yii DAO,PDO 擴展和特定的 PDO 數據庫驅動(例如 PDO_MYSQL) 必須安裝。
Yii DAO 主要包含如下四個類:

CDbConnection: 代表一個數據庫連接。
CDbCommand: 代表一條通過數據庫執行的 SQL 語句。
CDbDataReader: 代表一個只向前移動的,來自一個查詢結果集中的行的流。
CDbTransaction: 代表一個數據庫事務。

1、建立數據庫連接

要建立一個數據庫連接,創建一個 CDbConnection 實例并將其激活。連接到數據庫需要一個數據源的名字(DSN)以指定連接信息。用戶名和密碼也可能會用到。當連接到數據庫的過程中發生錯誤時 (例如,錯誤的 DSN 或無效的用戶名/密碼),將會拋出一個異常。

$connection=new CDbConnection($dsn,$username,$password);
// 建立連接。你可以使用  try...catch 捕獲可能拋出的異常
$connection->active=true;
......
$connection->active=false;  // 關閉連接

DSN 的格式取決于所使用的 PDO 數據庫驅動。總體來說, DSN 要含有 PDO 驅動的名字,跟上一個冒號,再跟上驅動特定的連接語法。可查閱 PDO 文檔 獲取更多信息。下面是一個常用DSN格式的列表。
* SQLite: sqlite:/path/to/dbfile
* MySQL: mysql:host=localhost;dbname=testdb
* PostgreSQL: pgsql:host=localhost;port=5432;dbname=testdb
* SQL Server: mssql:host=localhost;dbname=testdb
* Oracle: oci:dbname=//localhost:1521/testdb
由于 CDbConnection 繼承自 CApplicationComponent,我們也可以將其作為一個 應用組件 使用。要這樣做的話,請在 應用配置 中配置一個 db (或其他名字)應用組件如下:

array(
    ......
    'components'=>array(
        ......
        'db'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'mysql:host=localhost;dbname=testdb',
            'username'=>'root',
            'password'=>'password',
            'emulatePrepare'=>true,  // needed by some MySQL installations
        ),
    ),
)

然后我們就可以通過Yii::app()->db 訪問數據庫連接了。它已經被自動激活了,除非我們特意配置了 CDbConnection::autoConnect 為 false。通過這種方式,這個單獨的DB連接就可以在我們代碼中的很多地方共享。

2、執行SQL語句

數據庫連接建立后,SQL 語句就可以通過使用 CDbCommand 執行了。你可以通過使用指定的SQL語句作為參數調用 CDbConnection::createCommand()創建一個 CDbCommand 實例。

$connection=Yii::app()->db;   // 假設你已經建立了一個 "db" 連接
// 如果沒有,你可能需要顯式建立一個連接:
// $connection=new CDbConnection($dsn,$username,$password);
$command=$connection->createCommand($sql);
// 如果需要,此 SQL 語句可通過如下方式修改:
// $command->text=$newSQL;

一條 SQL 語句會通過 CDbCommand 以如下兩種方式被執行:
execute(): 執行一個無查詢 (non-query)SQL語句,例如 INSERT, UPDATE 和 DELETE 。如果成功,它將返回此執行所影響的行數。
query(): 執行一條會返回若干行數據的 SQL 語句,例如 SELECT。如果成功,它將返回一個 CDbDataReader 實例,通過此實例可以遍歷數據的結果行。為簡便起見,(Yii)還實現了一系列 queryXXX() 方法以直接返回查詢結果。
執行 SQL 語句時如果發生錯誤,將會拋出一個異常。

$rowCount=$command->execute();   // 執行無查詢SQL
$dataReader=$command->query();   // 執行一個SQL查詢
$rows=$command->queryAll();      // 查詢并返回結果中的所有行
$row=$command->queryRow();       // 查詢并返回結果中的第一行
$column=$command->queryColumn(); // 查詢并返回結果中的第一列
$value=$command->queryScalar();  // 查詢并返回結果中第一行的第一個字段

3、獲取查詢結果

CDbCommand::query() 生成 CDbDataReader 實例之后,你可以通過重復調用CDbDataReader::read() 獲取結果中的行。你也可以在 PHP 的 foreach 語言結構中使用 CDbDataReader 一行行檢索數據。

$dataReader=$command->query();
// 重復調用 read() 直到它返回 false
while(($row=$dataReader->read())!==false) { ... }
// 使用 foreach 遍歷數據中的每一行
foreach($dataReader as $row) { ... }
// 一次性提取所有行到一個數組
$rows=$dataReader->readAll();

注意: 不同于query(), 所有的queryXXX()方法會直接返回數據。例如,queryRow()會返回代表查詢結果第一行的一個數組。

4、使用事務

事務,在 Yii 中表現為 CDbTransaction 實例,可能會在下面的情況中啟動:
* 開始事務.
* 一個個執行查詢。任何對數據庫的更新對外界不可見。
* 提交事務。如果事務成功,更新變為可見。
* 如果查詢中的一個失敗,整個事務回滾。
上述工作流可以通過如下代碼實現:

$transaction=$connection->beginTransaction();
try
{
    $connection->createCommand($sql1)->execute();
    $connection->createCommand($sql2)->execute();
    //.... other SQL executions
    $transaction->commit();
}
catch(Exception $e) // 如果有一條查詢失敗,則會拋出異常
{
    $transaction->rollBack();
}

5、綁定參數

要避免 SQL 注入攻擊 并提高重復執行的 SQL 語句的效率,你可以 "準備(prepare)"一條含有可選參數占位符的 SQL 語句,在參數綁定時,這些占位符將被替換為實際的參數。
參數占位符可以是命名的 (表現為一個唯一的標記) 或未命名的 (表現為一個問號)。調用 CDbCommand::bindParam()CDbCommand::bindValue() 以使用實際參數替換這些占位符。這些參數不需要使用引號引起來:底層的數據庫驅動會為你搞定這個。參數綁定必須在 SQL 語句執行之前完成。

// 一條帶有兩個占位符 ":username" 和 ":email"的 SQL
$sql="INSERT INTO tbl_user (username, email) VALUES(:username,:email)";
$command=$connection->createCommand($sql);
// 用實際的用戶名替換占位符 ":username"
$command->bindParam(":username",$username,PDO::PARAM_STR);
// 用實際的 Email 替換占位符 ":email"
$command->bindParam(":email",$email,PDO::PARAM_STR);
$command->execute();
// 使用新的參數集插入另一行
$command->bindParam(":username",$username2,PDO::PARAM_STR);
$command->bindParam(":email",$email2,PDO::PARAM_STR);
$command->execute();

方法 bindParam() 和 bindValue() 非常相似。唯一的區別就是前者使用一個PHP變量綁定參數,而后者使用一個值。對于那些內存中的大數據塊參數,處于性能的考慮,應優先使用前者。

6、綁定列

當獲取查詢結果時,你也可以使用PHP變量綁定列。這樣在每次獲取查詢結果中的一行時就會自動使用最新的值填充。

$sql="SELECT username, email FROM tbl_user";
$dataReader=$connection->createCommand($sql)->query();
// 使用 $username 變量綁定第一列 (username)
$dataReader->bindColumn(1,$username);
// 使用 $email 變量綁定第二列 (email)
$dataReader->bindColumn(2,$email);
while($dataReader->read()!==false)
{
    // $username 和 $email 含有當前行中的 username 和 email
}

7、使用表前綴

要使用表前綴,配置 CDbConnection::tablePrefix 屬性為所希望的表前綴。然后,在 SQL 語句中使用 {{TableName}} 代表表的名字,其中的 TableName 是指不帶前綴的表名。例如,如果數據庫含有一個名為 tbl_user 的表,而 tbl_ 被配置為表前綴,那我們就可以使用如下代碼執行用戶相關的查詢:

$sql='SELECT * FROM {{user}}';
$users=$connection->createCommand($sql)->queryAll();

二、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并非要解決所有數據庫相關的任務。它的最佳應用是模型化數據表為PHP結構和執行不包含復雜SQL語句的查詢。 對于復雜查詢的場景,應使用Yii DAO。

1、建立數據庫連接

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屬性的值為一個大于零的值開啟表結構緩存。
如果你想使用一個不是db的應用組件,或者如果你想使用AR處理多個數據庫,你應該覆蓋CActiveRecord::getDbConnection() 。CActiveRecord類是所有AR類的基類。
提示: 通過AR使用多個數據庫有兩種方式。如果數據庫的結構不同,你可以創建不同的AR基類實現不同的getDbConnection()。否則,動態改變靜態變量CActiveRecord::db是一個好主意。

2、定義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()方法。
要使用表前綴功能,AR類的 tableName() 方法可以通過如下方式覆蓋

public function tableName()
{
    return '{{post}}';
}

這就是說,我們將沒有前綴的表名用雙大括號括起來,這樣Yii就能自動添加前綴,從而返回完整的表名。
數據表行中列的值可以作為相應AR實例的屬性訪問。例如,如下代碼設置了 title 列 (屬性):

$post=new Post;
$post->title='a sample post';

雖然我們從未在Post類中顯式定義屬性title,我們還是可以通過上述代碼訪問。這是因為title是tbl_post表中的一個列,CActiveRecord通過PHP的__get()魔術方法使其成為一個可訪問的屬性。如果我們嘗試以同樣的方式訪問一個不存在的列,將會拋出一個異常。
如果一個表沒有主鍵,則必須在相應的AR類中通過如下方式覆蓋 primaryKey() 方法指定哪一列或哪幾列作為主鍵。

public function primaryKey()
{
    return 'id';
    // 對于復合主鍵,要返回一個類似如下的數組
    // return array('pk1', 'pk2');
}

3、創建記錄

要向數據表中插入新行,我們要創建一個相應 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

記錄在保存(插入或更新)到數據庫之前,其屬性可以賦值為 CDbExpression 類型。例如,為保存一個由MySQL的 NOW() 函數返回的時間戳,我們可以使用如下代碼:

$post=new Post;
$post->create_time=new CDbexpression_r('NOW()'); //CDbExpression類就是計算數據庫表達式的值
// $post->create_time='NOW()'; 不會起作用,因為
// 'NOW()' 將會被作為一個字符串處理。
$post->save();

提示: 由于AR允許我們無需寫一大堆SQL語句就能執行數據庫操作, 我們經常會想知道AR在背后到底執行了什么SQL語句。這可以通過開啟Yii的日志功能實現。例如,我們在應用配置中開啟了CWebLogRoute,我們將會在每個網頁的最后看到執行過的SQL語句。 我們也可以在應用配置中設置CDbConnection::enableParamLogging為true,這樣綁定在SQL語句中的參數值也會被記錄。

4、讀取記錄

要讀取數據表中的數據,我們可以通過如下方式調用 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 則是一個參數數組,其中的值應綁定到 $condition 中的占位符。例如:

// 查找 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);

5、更新記錄

在 AR 實例填充了列的值之后,我們可以改變它們并把它們存回數據表。

$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // 將更改保存到數據庫

正如我們可以看到的,我們使用同樣的 save() 方法執行插入和更新操作。如果一個 AR 實例是使用 new 操作符創建的,調用 save() 將會向數據表中插入一行新數據;如果 AR 實例是某個findfindAll 方法的結果,調用 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在前面的段落中已有描述。

6、刪除記錄

如果一個 AR 實例被一行數據填充,我們也可以刪除此行數據。

$post=Post::model()->findByPk(10); // 假設有一個帖子,其 ID 為 10
$post->delete(); // 從數據表中刪除此行

注意,刪除之后, AR 實例仍然不變,但數據表中相應的行已經沒了。
使用下面的類級別代碼,可以無需首先加載行就可以刪除它。

// 刪除符合指定條件的行
Post::model()->deleteAll($condition,$params);
// 刪除符合指定條件和主鍵的行
Post::model()->deleteByPk($pk,$condition,$params);

7、數據驗證

當插入或更新一行時,我們常常需要檢查列的值是否符合相應的規則。如果列的值是由最終用戶提供的,這一點就更加重要。總體來說,我們永遠不能相信任何來自客戶端的數據。
當調用 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();

8、對比記錄

類似于表記錄,AR實例由其主鍵值來識別。因此,要對比兩個AR實例,假設它們屬于相同的AR類, 我們只需要對比它們的主鍵值。然而,一個更簡單的方式是調用 CActiveRecord::equals()
不同于AR在其他框架的執行, Yii在其 AR 中支持多個主鍵. 一個復合主鍵由兩個或更多字段構成。相應地,主鍵值在Yii中表現為一個數組。primaryKey屬性給出了一個 AR 實例的主鍵值。

9、自定義

CActiveRecord 提供了幾個占位符方法,它們可以在子類中被覆蓋以自定義其工作流。

beforeValidate 和 afterValidate:這兩個將在驗證數據有效性之前和之后被調用。
beforeSave 和 afterSave: 這兩個將在保存 AR 實例之前和之后被調用。
beforeDelete 和 afterDelete: 這兩個將在一個 AR 實例被刪除之前和之后被調用。
afterConstruct: 這個將在每個使用 new 操作符創建 AR 實例后被調用。
beforeFind: 這個將在一個 AR 查找器被用于執行查詢(例如 find(), findAll())之前被調用。
afterFind: 這個將在每個 AR 實例作為一個查詢結果創建時被調用。

10、使用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();
}

11、命名范圍

命名范圍(named scope)表示一個命名的(named)查詢規則,它可以和其他命名范圍聯合使用并應用于Active Record查詢。
命名范圍主要是在 CActiveRecord::scopes() 方法中以名字-規則對的方式聲明。如下代碼在Post模型類中聲明了兩個命名范圍, publishedrecently

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 DESClimit 屬性為 5。他們翻譯為查詢規則后就會返回最近的5篇帖子。
命名范圍多用作 find 方法調用的修改器。幾個命名范圍可以鏈到一起形成一個更有約束性的查詢結果集。例如,要找到最近發布的帖子,我們可以使用如下代碼:
$posts=Post::model()->published()->recently()->findAll();
總體來說,命名范圍必須出現在一個 find 方法調用的左邊。它們中的每一個都提供一個查詢規則,并聯合到其他規則,包括傳遞給 find 方法調用的那一個。最終結果就像給一個查詢添加了一系列過濾器。
命名范圍也可用于 updatedelete 方法。例如,如下代碼將刪除所有最近發布的帖子:
Post::model()->published()->recently()->delete();
注意: 命名范圍只能用于類級別方法。也就是說,此方法必須使用 ClassName::model() 調用。

12、參數化的命名范圍

命名范圍可以參數化。例如,我們想自定義 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 條最近發布的帖子。

13、默認的命名范圍

模型類可以有一個默認命名范圍,它將應用于所有 (包括相關的那些) 關于此模型的查詢。例如,一個支持多種語言的網站可能只想顯示當前用戶所指定的語言的內容。因為可能會有很多關于此網站內容的查詢,我們可以定義一個默認的命名范圍以解決此問題。為實現此目的,我們覆蓋 CActiveRecord::defaultScope 方法如下:

class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
            'condition'=>"language='".Yii::app()->language."'",
        );
    }
}

現在,如果下面的方法被調用,將會自動使用上面定義的查詢規則:
$contents=Content::model()->findAll();
注意,默認的命名范圍只會應用于 SELECT 查詢。INSERT, UPDATEDELETE 查詢將被忽略。

三、Relational Active Record(關聯查詢)

我們已經知道如何通過Active Record(AR)從單個數據表中取得數據了,在這一節中,我們將要介紹如何使用AR來連接關聯的數據表獲取數據。
在使用關聯AR之前,首先要在數據庫中建立關聯的數據表之間的主鍵-外鍵關聯,AR需要通過分析數據庫中的定義數據表關聯的元信息,來決定如何連接數據。

1、如何聲明關聯

在使用AR進行關聯查詢之前,我們需要告訴AR各個AR類之間有怎樣的關聯。
AR類之間的關聯直接反映著數據庫中這個類所代表的數據表之間的關聯。從關系數據庫的角度來說,兩個數據表A,B之間可能的關聯有三種:一對多,一對一,多對多。而在AR中,關聯有以下四種:

BELONGS_TO: 如果數據表A和B的關系是一對多,那我們就說B屬于A(B belongs to A)。
HAS_MANY: 如果數據表A和B的關系是多對一,那我們就說B有多個A(B has many A)。
HAS_ONE: 這是‘HAS_MANY’關系中的一個特例,當A最多有一個的時候,我們說B有一個A (B has one A)。
MANY_MANY: 這個相當于關系數據庫中的多對多關系。因為絕大多數關系數據庫并不直接支持多對多的關系,這時通常都需要一個單獨的關聯表,把多對多的關系分解為兩個一對多的關系。用AR的方式去理解的話,我們可以認為 MANY_MANY關系是由BELONGS_TO和HAS_MANY組成的。

在AR中聲明關聯,是通過覆蓋(Override)父類CActiveRecord中的relations()方法來實現的。這個方法返回一個包含了關系定義的數組,數組中的每一組鍵值代表一個關聯:
'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)
這里的VarName是這個關聯的名稱;RelationType指定了這個關聯的類型,有四個常量代表了四種關聯的類型:
self::BELONGS_TO,self::HAS_ONE,self::HAS_MANY和self::MANY_MANY; ClassName是這個關系關聯到的AR類的類名;ForeignKey指定了這個關聯是通過哪個外鍵聯系起來的。后面的additional options可以加入一些額外的設置,后面會做介紹。
下面的代碼演示了如何定義UserPost之間的關聯。

class Post extends CActiveRecord {
    public function relations() {
        return array(
                'author'=>array(
                    self::BELONGS_TO,
                     'User',
                     'authorID'
                    ),
                 'categories'=>array(
                    self::MANY_MANY,
                    'Category',
                    'PostCategory(postID, categoryID)'
                    ),
            );
    }
}

class User extends CActiveRecord {
    public function relations() {
        return array(
                'posts'=>array(
                    self::HAS_MANY,
                    'Post',
                    'authorID'
                    ),
                'profile'=>array(
                    self::HAS_ONE,
                    'Profile',
                    'ownerID'
                    ),
            );
    }
}

說明: 有時外鍵可能由兩個或更多字段組成,在這里可以將多個字段名由逗號或空格分隔, 一并寫在這里。對于多對多的關系,關聯表必須在外鍵中注明,例如在Post類的categories 關聯中,外鍵就需要寫成PostCategory(postID, categoryID)。
在AR類中聲明關聯時,每個關聯會作為一個屬性添加到AR類中,屬性名就是關聯的名稱。在進行關聯查詢時,這些屬性就會被設置為關聯到的AR類的實例,例如在查詢取得一個Post實例時,它的$author屬性就是代表Post作者的一個User類的實例。

2、關聯查詢

進行關聯查詢最簡單的方式就是訪問一個關聯AR對象的某個關聯屬性。如果這個屬性之前沒有被訪問過,這時就會啟動一個關聯查詢,通過當前AR對象的主鍵連接相關的表,來取得關聯對象的值,然后將這些數據保存在對象的屬性中。這種方式叫做“延遲加載”,也就是只有等到訪問到某個屬性時,才會真正到數據庫中把這些關聯的數據取出來。下面的例子描述了延遲加載的過程:

// retrieve the post whose ID is 10
$post=Post::model()->findByPk(10);
// retrieve the post's author: a relational query will be performed here
$author=$post->author;

在不同的關聯情況下,如果沒有查詢到結果,其返回的值也不同:BELONGS_TOHAS_ONE 關聯,無結果時返回null; HAS_MANYMANY_MANY, 無結果時返回空數組。
延遲加載方法使用非常方便,但在某些情況下并不高效。例如,若我們要取得N個post的作者信息,使用延遲方法將執行N次連接查詢。此時我們應當使用所謂的急切加載方法。
急切加載方法檢索主要的 AR 實例及其相關的 AR 實例. 這通過使用 with() 方法加上 findfindAll 方法完成。例如,
$posts=Post::model()->with('author')->findAll();
上面的代碼將返回一個由 Post 實例組成的數組. 不同于延遲加載方法,每個Post 實例中的author 屬性在我們訪問此屬性之前已經被關聯的 User 實例填充。不是為每個post 執行一個連接查詢, 急切加載方法在一個單獨的連接查詢中取出所有的 post 以及它們的author!
我們可以在with()方法中指定多個關聯名字。例如, 下面的代碼將取回 posts 以及它們的作者和分類:
$posts=Post::model()->with('author','categories')->findAll();
我們也可以使用嵌套的急切加載。不使用一個關聯名字列表, 我們將關聯名字以分層的方式傳遞到 with() 方法, 如下,

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

上面的代碼將取回所有的 posts 以及它們的作者和分類。它也將取出每個作者的profileposts.
急切加載也可以通過指定 CDbCriteria::with 屬性被執行, 如下:

$criteria=new CDbCriteria;
$criteria->with=array(
'author.profile',
    'author.posts',
    'categories',
);
$posts=Post::model()->findAll($criteria);
或
$posts=Post::model()->findAll(array(
'with'=>array(
        'author.profile',
        'author.posts',
        'categories',
    )
);

3、關聯查詢選項

之前我們提到額外的參數可以被指定在關聯聲明中。這些選項,指定為 name-value 對,被用來定制關聯查詢。它們被概述如下:

select: 為關聯 AR 類查詢的字段列表。默認是 '*', 意味著所有字段。
        查詢的字段名字可用別名表達式來消除歧義(例如:COUNT(??.name) AS nameCount)。
condition: WHERE 子語句。默認為空。注意, 列要使用別名引用(例如:??.id=10)。
params: 被綁定到 SQL 語句的參數. 應當為一個由 name-value 對組成的數組()。
on: ON 子語句. 這里指定的條件將使用 and 操作符被追加到連接條件中。
       此選項中的字段名應被消除歧義。此選項不適用于 MANY_MANY 關聯。
order: ORDER BY 子語句。默認為空。注意, 列要使用別名引用(例如:??.age DESC)。
with: 應當和此對象一同載入的子關聯對象列表. 注意, 不恰當的使用可能會形成一個無窮的關聯循環。
joinType: 此關聯的連接類型。默認是 LEFT OUTER JOIN。
aliasToken:列前綴占位符。默認是“??.”。
alias: 關聯的數據表的別名。默認是 null, 意味著表的別名和關聯的名字相同。
together: 是否關聯的數據表被強制與主表和其他表連接。此選項只對于HAS_MANY 和 MANY_MANY 關聯有意義。
          若此選項被設置為 false, ......(此處原文出錯!).默認為空。此選項中的字段名以被消除歧義。
having: HAVING 子語句。默認是空。注意, 列要使用別名引用。
index: 返回的數組索引類型。確定返回的數組是關鍵字索引數組還是數字索引數組。
       不設置此選項, 將使用數字索引數組。此選項只對于HAS_MANY 和 MANY_MANY 有意義
       此外, 下面的選項在延遲加載中對特定關聯是可用的:
group: GROUP BY子句。默認為空。注意, 列要使用別名引用(例如:??.age)。 
       本選項僅應用于HAS_MANY 和 MANY_MANY 關聯。
having: HAVING子句。默認為空。注意, 列要使用別名引用(例如:??.age)。
       本選項僅應用于HAS_MANY 和 MANY_MANY 關聯。
limit: 限制查詢的行數。本選項不能用于BELONGS_TO關聯。
offset: 偏移。本選項不能用于BELONGS_TO關聯。

下面我們改變在 User 中的 posts 關聯聲明,通過使用上面的一些選項:

class User extends CActiveRecord 
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                            'order'=>'posts.create_time DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
        );
    }
}

現在若我們訪問 $author->posts, 我們將得到用戶的根據發表時間降序排列的 posts. 每個 post 實例也載入了它的分類。

4、為字段名消除歧義

當一個字段的名字出現在被連接在一起的兩個或更多表中,需要消除歧義(disambiguated)。可以通過使用表的別名作為字段名的前綴實現。
在關聯AR查詢中,主表的別名確定為 t,而一個關聯表的別名和相應的關聯的名字相同(默認情況下)。 例如,在下面的語句中,Post 的別名是 t ,而 Comment 的別名是 comments:
$posts=Post::model()->with('comments')->findAll();
現在假設 PostComment 都有一個字段 create_time , 我們希望取出 posts 及它們的 comments ,排序方式是先根據 posts 的創建時間,然后根據 comment 的創建時間。 我們需要消除create_time 字段的歧義,如下:

$posts=Post::model()->with('comments')->findAll(array(
    'order'=>'t.create_time, comments.create_time'
));

默認情況下,Yii 自動為每個關聯表產生一個表別名,我們必須使用此前綴 ??. 來指向這個自動產生的別名。 主表的別名是表自身的名字。

5、動態關聯查詢選項

我們使用 with()with 均可使用動態關聯查詢選項。 動態選項將覆蓋在 relations() 方法中指定的
已存在的選項。例如,使用上面的 User 模型, 若我們想要使用急切加載方法以升序來取出屬于一個作者的 posts(關聯中的order 選項指定為降序), 我們可以這樣做:

User::model()->with(array(
    'posts'=>array('order'=>'posts.create_time ASC'),
    'profile',
))->findAll();

動態查詢選項也可以在使用延遲加載方法時使用以執行關聯查詢。 要這樣做,我們應當調用一個方法,它的名字和關聯的名字相同,并傳遞動態查詢選項 作為此方法的參數。例如,下面的代碼返回一個用戶的 status 為 1 的posts :

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

6、關聯查詢的性能

如上所述,急切加載方法主要用于當我們需要訪問許多關聯對象時。 通過連接所有所需的表它產生一個大而復雜的 SQL 語句。一個大的 SQL 語句在許多情況下是首選的。然而在一些情況下它并不高效。
考慮一個例子,若我們需要找出最新的文章以及它們的評論。 假設每個文章有 10 條評論,使用一個大的 SQL 語句,我們將取回很多多余的 post 數據, 因為每個post 將被它的每條評論反復使用。現在讓我們嘗試另外的方法:我們首先查詢最新的文章, 然后查詢它們的評論。用新的方法,我們需要執行執行兩條 SQL 語句。有點是在查詢結果中沒有多余的數據。
因此哪種方法更加高效?沒有絕對的答案。執行一條大的 SQL 語句也許更加高效,因為它需要更少的花銷來解析和執行 SQL 語句。另一方面,使用單條 SQL 語句,我們得到更多冗余的數據,因此需要更多時間來閱讀和處理它們。 因為這個原因,Yii 提供了 together 查詢選項一邊我們在需要時選擇兩種方法之一。默認下, Yii 使用第一種方式,即產生一個單獨的 SQL 語句來執行急切加載。我們可以在關聯聲明中設置 together 選項為 false 以便一些表被連接在單獨的 SQL 語句中。例如,為了使用第二種方法來查詢最新的文章及它們的評論,我們可以在 Post 類中聲明 comments 關聯如下,

public function relations()
{
    return array(
        'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false),
    );
}

當我們執行急切加載時,我們也可以動態地設置此選項:
$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();

7、統計查詢

除了上面描述的關聯查詢,Yii 也支持所謂的統計查詢(或聚合查詢)。 它指的是檢索關聯對象的聚合信息,例如每個 post 的評論的數量,每個產品的平均等級等。 統計查詢只被 HAS_MANY(例如,一個 post 有很多評論) 或 MANY_MANY (例如,一個post 屬于很多分類和一個 category 有很多 post) 關聯對象執行。
執行統計查詢非常類似于之前描述的關聯查詢。我們首先需要在 CActiveRecordrelations() 方法中聲明統計查詢。

class Post extends CActiveRecord
{
    public function relations()
   {
        return array(
            'commentCount'=>array(self::STAT, 'Comment', 'post_id'),
            'categoryCount'=>array(self::STAT, 'Category', 'post_category(post_id,
category_id)'),
        );
    }
}

在上面,我們聲明了兩個統計查詢:commentCount 計算屬于一個 post 的評論的數量,categoryCount 計算一個 post 所屬分類的數量。注意 PostComment 之間的關聯類型是 HAS_MANY, 而 PostCategory 之間的關聯類型是 MANY_MANY (使用連接表 PostCategory)。 如我們所看到的,聲明非常類似于之間小節中的關聯。唯一的不同是這里的關聯類型是 STAT
有了上面的聲明,我們可以檢索使用表達式 $post->commentCount 檢索一個 post 的評論的數量。 當我們首次訪問此屬性,一個 SQL 語句將被隱含地執行并檢索 對應的結果。我們已經知道,這是所謂的 lazy loading 方法。若我們需要得到多個post 的評論數目,我們也可以使用 eager loading 方法:
$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();
上面的語句將執行三個 SQL 語句以取回所有的 post 及它們的評論數目和分類數目。使用延遲加載方法, 若有 N 個 post ,我們使用 2*N+1 條 SQL 查詢完成。
默認情況下,一個統計查詢將計算 COUNT 表達式(and thus the comment count and category count in the above example). 當我們在 relations()中聲明它時,通過 指定額外的選項,可以定制它。可用的選項簡介如下。

select:       統計表達式。默認是 COUNT(*),意味著子對象的個數。
defaultValue: 沒有接收一個統計查詢結果時被賦予的值。例如,若一個 post 沒有任何評論,
              它的 commentCount 將接收此值。此選項的默認值是 0。
condition:    WHERE 子語句。默認是空。
params:       被綁定到產生的SQL 語句中的參數。它應當是一個 name-value 對組成的數組。
order:        ORDER BY 子語句。默認是空。
group:        GROUP BY 子語句。默認是空。
having:       HAVING 子語句。默認是空。

8、關聯查詢命名空間

關聯查詢也可以和 命名空間一起執行。有兩種形式。第一種形式,命名空間被應用到主模型。第二種形式,命名空間被應用到關聯模型。
下面的代碼展示了如何應用命名空間到主模型。
$posts=Post::model()->published()->recently()->with('comments')->findAll();
這非常類似于非關聯的查詢。唯一的不同是我們在命名空間后使用了 with() 調用。 此查詢應當返回最近發布的 post和它們的評論。
下面的代碼展示了如何應用命名空間到關聯模型。
$posts=Post::model()->with('comments:recently:approved')->findAll();
上面的查詢將返回所有的 post 及它們審核后的評論。注意 comments 指的是關聯名字,而recentlyapproved 指的是 在 Comment 模型類中聲明的命名空間。關聯名字和命名空間應當由冒號分隔。
命名空間也可以在 CActiveRecord::relations() 中聲明的關聯規則的 with 選項中指定。在下面的例子中, 若我們訪問 $user->posts,它將返回此post 的所有審核后的評論。

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                'with'=>'comments:approved'),
        );
    }
}

注意: 應用到關聯模型的命名空間必須在 CActiveRecord::scopes 中指定。結果,它們不能被參數化。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容