咱們已經瞭解了怎樣使用 Active Record (AR) 從單個數據表中獲取數據。 在本節中,咱們講解怎樣使用 AR 鏈接多個相關數據表並取回關聯(join)後的數據集。數據庫
爲了使用關係型 AR,咱們建議在須要關聯的表中定義主鍵-外鍵約束。這些約束能夠幫助保證相關數據的一致性和完整性。express
爲簡單起見,咱們使用以下所示的實體-關係(ER)圖中的數據結構演示此節中的例子。數組
信息: 對外鍵約束的支持在不一樣的 DBMS 中是不同的。 SQLite < 3.6.19 不支持外鍵約束,但你依然能夠在建表時聲明約束。數據結構
在咱們使用 AR 執行關聯查詢以前,咱們須要讓 AR 知道一個 AR 類是怎樣關聯到另外一個的。app
兩個 AR 類之間的關係直接經過 AR 類所表明的數據表之間的關係相關聯。 從數據庫的角度來講,表 A 和 B 之間有三種關係:一對多(one-to-many,例如 tbl_user 和 tbl_post),一對一( one-to-one 例如 tbl_user 和 tbl_profile)和 多對多(many-to-many 例如 tbl_category 和 tbl_post)。 在 AR 中,有四種關係:框架
BELONGS_TO(屬於): 若是表 A 和 B 之間的關係是一對多,則 表 B 屬於 表 A (例如 Post 屬於 User); HAS_MANY(有多個): 若是表 A 和 B 之間的關係是一對多,則 A 有多個 B (例如 User 有多個 Post); HAS_ONE(有一個): 這是 HAS_MANY 的一個特例,A 最多有一個 B (例如 User 最多有一個 Profile); MANY_MANY: 這個對應於數據庫中的多對多關係。 因爲多數 DBMS 不直接支持 多對多 關係,所以須要有一個關聯表將 多對多 關係分割爲 一對多 關係。 在咱們的示例數據結構中,tbl_post_category 就是用於此目的的。在 AR 術語中,咱們能夠解釋 MANY_MANY 爲 BELONGS_TO 和 HAS_MANY 的組合。 例如,Post 屬於多個(belongs to many) Category ,Category 有多個(has many) Post.
AR 中定義關係須要覆蓋 CActiveRecord 中的 relations() 方法。此方法返回一個關係配置數組。每一個數組元素經過以下格式表示一個單一的關係。less
'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)
其中 VarName 是關係的名字;RelationType 指定關係類型,能夠是一下四個常量之一: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY and self::MANY_MANY;ClassName 是此 AR 類所關聯的 AR 類的名字; ForeignKey 指定關係中使用的外鍵(一個或多個)。額外的選項能夠在每一個關係的最後指定(稍後詳述)。yii
如下代碼演示了怎樣定義 User 和 Post 類的關係:ide
class Post extends CActiveRecord { ...... public function relations() { return array( 'author'=>array(self::BELONGS_TO, 'User', 'author_id'), 'categories'=>array(self::MANY_MANY, 'Category', 'tbl_post_category(post_id, category_id)'), ); } } class User extends CActiveRecord { ...... public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id'), 'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'), ); } }
信息: 外鍵多是複合的,包含兩個或更多個列。 這種狀況下,咱們應該將這些外鍵名字連接,中間用空格或逗號分割。對於 MANY_MANY 關係類型, 關聯表的名字必須也必須在外鍵中指定。例如, Post 中的 categories 關係由外鍵 tbl_post_category(post_id, category_id) 指定。post
AR 類中的關係定義爲每一個關係向類中隱式添加了一個屬性。在一個關聯查詢執行後,相應的屬性將將被以關聯的 AR 實例填充。 例如,若是 $author 表明一個 User AR 實例, 咱們可使用 $author->posts 訪問其關聯的 Post 實例。
執行關聯查詢最簡單的方法是讀取一個 AR 實例中的關聯屬性。若是此屬性之前沒有被訪問過,則一個關聯查詢將被初始化,它將兩個表關聯並使用當前 AR 實例的主鍵過濾。 查詢結果將以所關聯 AR 類的實例的方式保存到屬性中。這就是傳說中的 懶惰式加載(lazy loading,也可譯爲 遲加載) 方式,例如,關聯查詢只在關聯的對象首次被訪問時執行。 下面的例子演示了怎樣使用這種方式:
// 獲取 ID 爲 10 的帖子 $post=Post::model()->findByPk(10); // 獲取帖子的做者(author): 此處將執行一個關聯查詢。 $author=$post->author;
信息: 若是關係中沒有相關的實例,則相應的屬性將爲 null 或一個空數組。 BELONGS_TO 和 HAS_ONE 關係的結果是 null, HAS_MANY 和 MANY_MANY 的結果是一個空數組。 注意, HAS_MANY 和 MANY_MANY 關係返回對象數組,你須要在訪問任何屬性以前先遍歷這些結果。 不然,你可能會收到 "Trying to get property of non-object(嘗試訪問非對象的屬性)" 錯誤。
懶惰式加載用起來很方便,但在某些狀況下並不高效。若是咱們想獲取 N 個帖子的做者,使用這種懶惰式加載將會致使執行 N 個關聯查詢。 這種狀況下,咱們應該改成使用 渴求式加載(eager loading)方式。
渴求式加載方式會在獲取主 AR 實例的同時獲取關聯的 AR 實例。 這是經過在使用 AR 中的 find 或 findAll 方法時配合使用 with 方法完成的。例如:
$posts=Post::model()->with('author')->findAll();
上述代碼將返回一個 Post 實例的數組。與懶惰式加載方式不一樣,在咱們訪問每一個 Post 實例中的 author 屬性以前,它就已經被關聯的 User 實例填充了。 渴求式加載經過 一個 關聯查詢返回全部帖子及其做者,而不是對每一個帖子執行一次關聯查詢。
咱們能夠在 with() 方法中指定多個關係名字,渴求式加載將一次性所有取回他們。例如,以下代碼會將帖子連同其做者和分類一併取回。
$posts=Post::model()->with('author','categories')->findAll();
咱們也能夠實現嵌套的渴求式加載。像下面這樣, 咱們傳遞一個分等級的關係名錶達式到 with() 方法,而不是一個關係名列表:
$posts=Post::model()->with( 'author.profile', 'author.posts', 'categories' )->findAll();
上述示例將取回全部帖子及其做者和所屬分類。它還同時取回每一個做者的簡介(author.profile)和帖子(author.posts)。
從版本 1.1.0 開始,渴求式加載也能夠經過指定 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', ) );
咱們提到在關係聲明時能夠指定附加的選項。這些 名-值 對形式的選項用於自定義關係型查詢。歸納以下:
select: 關聯的 AR 類中要選擇(select)的列的列表。 默認爲 '*',即選擇全部列。此選項中的列名應該是已經消除歧義的。 condition: 即 WHERE 條件。默認爲空。此選項中的列名應該是已經消除歧義的。 params: 要綁定到所生成的 SQL 語句的參數。應該以 名-值 對數組的形式賦值。此選項從 1.0.3 版起有效。 on: 即 ON 語句。此處指定的條件將會經過 AND 操做符附加到 join 條件中。此選項中的列名應該是已經消除歧義的。 此選項不會應用到 MANY_MANY 關係中。此選項從 1.0.2 版起有效。 order: 即 ORDER BY 語句。默認爲空。 此選項中的列名應該是已經消除歧義的。 with: 應該和該對象一塊兒加載的一些列子相關對象. 注意不適當的使用該選項可能形成無限關係循環. joinType: 該關係的join類型. 默認是LEFT OUTER JOIN. alias: 和該關係關聯的表的別名. 這個選項從yii版本1.0.1起有效. 默認是null, 意味着表別名和關係名稱同樣. together: 該關係所關聯的表是否應該強制和主表和其餘表聯接. 這個選項只對HAS_MANY 和 MANY_MANY 這兩種關係有意義. 若是這個選項設置爲false, 那麼HAS_MANY或者 MANY_MANY 關係所關聯的表將會和主表在相互隔離的SQL查詢中聯接, 這將會提升整個查詢的性能,由於這會返回較少的重複數據. 若是這個選項設置爲true, 關聯的表總會和主表聯接在一個SQL查詢中, 即便主表是分頁的. 若是這個選項沒有設置,關聯表只有主表不是分頁的狀況下才會和主表聯接在一個SQL查詢中. 更多細節,請查看章節 "關係查詢性能". 這個選項從版本1.0.3開始支持. join: 額外的 JOIN 條款. 默認是空. 這個選項從版本1.1.3開始支持. group: GROUP BY 條款. 默認是空. 在該選項中列名的使用應該是無歧義的. having: HAVING 條款. 默認是空. 在該選項中列名的使用應該是無歧義的. 注意: 從版本1.0.1開始支持該選項. index: 列名被用於存儲關係對象數組的鍵值. 若是表不設置這個選項, 關係對象數組將會使用從0開始的整型索引.這個選項只能用於設置HAS_MANY 和 MANY_MANY 關係類型. yii框架從版本1.0.7之後開始支持該選項
此外, 下面這些選項在懶惰式加載中對特定關係是有效的:
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, 就會獲得基於建立時間排序的author's posts,而且每個post 實例都會加載其分類.
當兩表或多表聯接出現同一個列名時, 須要排除歧義.這能夠經過給列名加上表別名前綴來實現.
在關係 AR 查詢中, 主表別名默認是 t, 同時關係表的別名默認是相應的關係名稱.例如, 在下面的語句中, Post 和 Comment的別名分別是t 和 comments:
$posts=Post::model()->with('comments')->findAll();
如今假設 Post 和 Comment 都有一個代表其建立時間的列叫作create_time,而且咱們想要將posts和其對應的comments放在一塊兒查詢,排序方式首先是posts的建立時間,而後是comments的建立時間。咱們須要按照以下方式消除列名歧義:
$posts=Post::model()->with('comments')->findAll(array( 'order'=>'t.create_time, comments.create_time' ));
注意: 列歧義的行爲從版本1.1.0起有所改變. 在版本1.0.x中,默認Yii會爲每個關係表自動生成表別名, 而且咱們必須使用前綴??. 來引用自動生成的別名. 此外,在1.0.x版本中, 主表別名就是表名自己
從版本1.0.2開始,咱們能夠在with()和with選項中使用動態關係查詢選項. 動態選項會覆蓋已存在的如relations()方法中所指定的選項. 例如,在上面的User 模型中, 若是咱們想要使用渴求式加載方式以升序爲每個author附帶 posts (在關係定義中默認排序是降序), 能夠按照以下方式:
User::model()->with(array( 'posts'=>array('order'=>'posts.create_time ASC'), 'profile', ))->findAll();
從版本1.0.5開始, 動態查詢選項能夠在使用懶惰式加載方式進行關係查詢的時候使用. 咱們能夠調用一個關係名稱相同的方法而且傳入動態查詢選項做爲參數. 例如, 下面的代碼返回status爲1的 user's posts:
$user=User::model()->findByPk(1); $posts=$user->posts(array('condition'=>'status=1'));
As we described above, the eager loading approach is mainly used in the scenario when we need to access many related objects. It generates a big complex SQL statement by joining all needed tables. A big SQL statement is preferrable in many cases since it simplifies filtering based on a column in a related table. It may not be efficient in some cases, however.
Consider an example where we need to find the latest posts together with their comments. Assuming each post has 10 comments, using a single big SQL statement, we will bring back a lot of redundant post data since each post will be repeated for every comment it has. Now let's try another approach: we first query for the latest posts, and then query for their comments. In this new approach, we need to execute two SQL statements. The benefit is that there is no redundancy in the query results.
So which approach is more efficient? There is no absolute answer. Executing a single big SQL statement may be more efficient because it causes less overhead in DBMS for yparsing and executing the SQL statements. On the other hand, using the single SQL statement, we end up with more redundant data and thus need more time to read and process them.
For this reason, Yii provides the together query option so that we choose between the two approaches as needed. By default, Yii adopts the first approach, i.e., generating a single SQL statement to perform eager loading. We can set the together option to be false in the relation declarations so that some of tables are joined in separate SQL statements. For example, in order to use the second approach to query for the latest posts with their comments, we can declare the comments relation in Post class as follows,
public function relations() { return array( 'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false), ); }
We can also dynamically set this option when we perform the eager loading:
$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();
Note: In version 1.0.x, the default behavior is that Yii will generate and execute N+1 SQL statements if there are N HAS_MANY or MANY_MANY relations. Each HAS_MANY or MANY_MANY relation has its own SQL statement. By calling the together() method after with(), we can enforce only a single SQL statement is generated and executed. For example,
$posts=Post::model()->with( 'author.profile', 'author.posts', 'categories' )->together()->findAll();
Note: Statistical query has been supported since version 1.0.4.
Besides the relational query described above, Yii also supports the so-called statistical query (or aggregational query). It refers to retrieving the aggregational information about the related objects, such as the number of comments for each post, the average rating for each product, etc. Statistical query can only be performed for objects related in HAS_MANY (e.g. a post has many comments) or MANY_MANY (e.g. a post belongs to many categories and a category has many posts).
Performing statistical query is very similar to performing relation query as we described before. We first need to declare the statistical query in the relations() method of CActiveRecord like we do with relational query.
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)'), ); } }
In the above, we declare two statistical queries: commentCount calculates the number of comments belonging to a post, and categoryCount calculates the number of categories that a post belongs to. Note that the relationship between Post and Comment is HAS_MANY, while the relationship between Post and Category is MANY_MANY (with the joining table post_category). As we can see, the declaration is very similar to those relations we described in earlier subsections. The only difference is that the relation type is STAT here.
With the above declaration, we can retrieve the number of comments for a post using the expression $post->commentCount. When we access this property for the first time, a SQL statement will be executed implicitly to retrieve the corresponding result. As we already know, this is the so-called lazy loading approach. We can also use the eager loading approach if we need to determine the comment count for multiple posts:
$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();
The above statement will execute three SQLs to bring back all posts together with their comment counts and category counts. Using the lazy loading approach, we would end up with 2*N+1 SQL queries if there are N posts.
By default, a statistical query will calculate the COUNT expression (and thus the comment count and category count in the above example). We can customize it by specifying additional options when we declare it in relations(). The available options are summarized as below.
select: the statistical expression. Defaults to COUNT(*), meaning the count of child objects. defaultValue: the value to be assigned to those records that do not receive a statistical query result. For example, if a post does not have any comments, its commentCount would receive this value. The default value for this option is 0. condition: the WHERE clause. It defaults to empty. params: the parameters to be bound to the generated SQL statement. This should be given as an array of name-value pairs. order: the ORDER BY clause. It defaults to empty. group: the GROUP BY clause. It defaults to empty. having: the HAVING clause. It defaults to empty.
Note: The support for named scopes has been available since version 1.0.5.
Relational query can also be performed in combination with named scopes. It comes in two forms. In the first form, named scopes are applied to the main model. In the second form, named scopes are applied to the related models.
The following code shows how to apply named scopes to the main model.
$posts=Post::model()->published()->recently()->with('comments')->findAll();
This is very similar to non-relational queries. The only difference is that we have the with() call after the named-scope chain. This query would bring back recently published posts together with their comments.
And the following code shows how to apply named scopes to the related models.
$posts=Post::model()->with('comments:recently:approved')->findAll();
The above query will bring back all posts together with their approved comments. Note that comments refers to the relation name, while recently and approved refer to two named scopes declared in the Comment model class. The relation name and the named scopes should be separated by colons.
Named scopes can also be specified in the with option of the relational rules declared in CActiveRecord::relations(). In the following example, if we access $user->posts, it would bring back all approved comments of the posts.
class User extends CActiveRecord { public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id', 'with'=>'comments:approved'), ); } }
Note: Named scopes applied to related models must be specified in CActiveRecord::scopes. As a result, they cannot be parameterized.