數據庫之關係型AR

我們已經知道了如何用AR來讀取單個數據表中的數據。在本節中,我們將介紹如何用AR來讀取多個關聯表的數據。

要用關係型AR的話,強烈建議那些需要連接的表,做好主-外鍵的約束關係。這種約束關係可以保證這些關聯數據的一致性。爲了方便易懂,我們用一張數據庫的結構圖來作爲本節的關係圖。

1.1 聲明關係

在我們用AR來做關聯查詢之前,我們要讓AR知道一個AR類是怎麼關聯另外一個類的。

 

兩個AR類之間的關係,直接代表了數據庫中兩個表的關係。從數據庫的角度看,兩張表A與B之間有3中關係:one-to-many(e.g. tbl user and tblpost) , one-to-one (e.g. tbl user and tbl profile) 以及many-to-many (e.g. tbl categoryand tbl post)。 在AR中,有4中關係類型:

● BELONGS TO:  如果兩個表之間的關係是 one-to-many,那麼 B belongsto A (e.g. Post belongs to User);
● HAS MANY: if the relationship between table A and B is one-to-many, then A has manyB (e.g. User has many Post);
● HAS ONE: this is special case of HAS MANY where A has at most one B (e.g. User hasat most one Profile);
● MANY MANY: this corresponds to the many-to-many relationship in database. An as-sociative table is needed to break a many-to-many relationship into one-to-many relationships, as most DBMS do not surpport many-to-many relationship directly.In our example database schema, the tbl post category serves for this purpose. In AR terminology, we can explain MANY MANY as the combination of BELONGS TO and HAS MANY. For example, Post belongs to many Category and Category has many Post.

聲明AR關係,涉及到重載CActiveRecord的relations()的方法。該方法返回一組關係配置。每個數組元素表示一條對應關係:

  1. 'VarName'=>array('RelationType''ClassName''ForeignKey', ...additional options) 

VarName:關係的名稱
RelationType:表明這條關係是哪個類型(之前提到的4種關係類型)
ClassName:關聯的AR類名。
ForeignKey:關聯的外鍵

下面的代碼演示瞭如果定義User 和Post的關係。

  1. class Post extends CActiveRecord  
  2. {  
  3.     ......  
  4.     public function relations()  
  5.     {  
  6.         return array(  
  7.             'author'=>array(self::BELONGS TO, 'User''author id'),  
  8.             'categories'=>array(self::MANY MANY, 'Category',  
  9.             'tbl post category(post id, category id)'),  
  10.         );  
  11.     }  
  12. }  
  13. class User extends CActiveRecord  
  14. {  
  15.     ......  
  16.     public function relations()  
  17.     {  
  18.         return array(  
  19.             'posts'=>array(self::HAS MANY, 'Post''author id'),  
  20.             'profile'=>array(self::HAS ONE, 'Profile''owner id'),  
  21.         );  
  22.     }  

INFO:外鍵可能是一個聯合組建。

1.2 執行關聯查詢

最簡單的關聯查詢方法是讀取一個關聯屬性的AR實例。如果該屬性之前沒被訪問過,一個關聯插敘會被初始化,用來關聯這個AR實例所關聯的表。查詢結果會保存到對應的AR實例。這個就是著名的後裝載方法。關聯操作只有在對應的對象被訪問時才執行操作。下面示範一下怎麼使用這個方法:

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

INFO:如果不存在對應關係的實例,那麼當前的屬性可能是null或者是一個空的數組。對於BELONG_TO和HAS_ONE,結果是null,對於HAS_MANY和MANY_MANY,結果是一個空的數組。請注意,HAS_MANY以及MANY_MANY返回的是一組對象,在想調用他們的屬性之前,必須先遍歷數組。否則,你會得到"Trying to get property of non-object"的錯誤提示。

後裝載的方法使用很便捷,但是在某些場景下不是很高效。例如,我們想要知道N篇文章的作者,我們必須調用N個關聯查詢語句。在這種情況下,我們應該調用的是先裝載的方法。

預裝載的方法,事先就捕獲了相關的AR實例,以及主的AR實例。這些是有whth()方法加上一個find()或者是findAll()的方法實現的。例如:

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

上面的代碼會返回一個Post的對象數組。跟後裝載不同的是,在我們調用author這個屬性之前,他就已經被關聯的User對象給裝載好了。預裝載的方法,在實例化的時候就把所有該作者的文章都給關聯出來了,在一個但一個查詢的語句中。我們可以在with()方法中指定多個關係,預裝載會一次把他們給查詢出來。例如,接下來的代碼將實現返回文章,以及關聯的作者,文章分類。

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

 

我們也可以實現嵌套的預裝載。我們不用一串關聯名稱的方法,我們通過向with方法傳遞一個結構來代表這些關係,如下:

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

上面的示例中,將返回所有的文章(Post)以及他們的作者,分類。同時也返回了作者的配置信息以及他的所有文章。

預裝載也可以用設定CDbCriteria::with 屬性的方法,如下:

  1. $criteria=new CDbCriteria;  
  2.     $criteria->with=array(  
  3.     'author.profile',  
  4.     'author.posts',  
  5.     'categories',  
  6. ); 
  7. $posts=Post::model()->findAll($criteria);

或者是

  1. $posts=Post::model()->findAll(array(  
  2.     'with'=>array(  
  3.         'author.profile',  
  4.         'author.posts',  
  5.         'categories',  
  6.     )  
  7. )); 

1.3 不獲取相關模型的情況下執行關聯查詢

有些時候,我們想執行關聯查詢的動作,但是又不想獲取涉及的模型。假設我們的Users有很多的Posts。post可以是發佈狀態,也可能是草稿的狀態。這取決於Posts表中的published字段。現在我們想得到發佈過文章的Users,但是又對他所發佈的文章一點興趣都沒有,這時候我們可以這樣做:

  1. $users=User::model()->with(array(  
  2.     'posts'=>array(  
  3.         // we don't want to select posts  
  4.         'select'=>false,  
  5.         // but want to get only users with published posts  
  6.         'joinType'=>'INNER JOIN',  
  7.         'condition'=>'posts.published=1',  
  8.     ),  
  9. ))->findAll(); 

1.4 關聯查詢選項

我們提到過,可選選項可以用來指明關係類型。這些選項,也是用name-value的格式。總結如下:

SELECT , CONDITION, PARAMS , PARAMS , ON , ORDER , WITH , JOINTYPE , ALIAS , TOGETHER , JOIN , GROUP , HAVING , INDEX , SCOPES ,

另外,後裝載的選項:limit , offset , through

下例中,我們用上面的一些方法來修改posts的User關係:

  1. class User extends CActiveRecord  
  2. {  
  3.     public function relations()  
  4.     {  
  5.         return array(  
  6.             'posts'=>array(self::HAS_MANY, 'Post''author id',  
  7.             'order'=>'posts.create time DESC',  
  8.             'with'=>'categories'),  
  9.             'profile'=>array(self::HAS_ONE, 'Profile''owner id'),  
  10.         );  
  11.     }  

現在,如果我們訪問$author->posts, 我們會獲取到該作者的所有文章,按照時間倒序。每篇文章實例的分類也都已經裝載好了。

1.5 理清冗餘的字段名

當一個字段名在連接的多個表中出現時,需要理清楚的。這些通過通過在字段名前加上表別名來實現。

在關聯AR查詢時,主表的別名一般爲t ,而相關聯的表別名就是默認名。例如,下面表的別名對應爲Post t, Coments coments:

  1. $posts=Post::model()->with('comments')->findAll(); 

好了,現在假設Post 以及Coment都有一個字段名爲create_time,用來記錄創建的時間,現在我們都想獲得這兩個時間,先按照Post的時間,然後再是Coment的時間。我們需要理清楚create_time這個字段:

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

1.6 動態關聯查詢

我們可以用with()以及其選項的方法來實現動態查詢。這些動態選項會覆蓋現有的relations方法。例如,上例中User模型,如果我們想用預裝載的方法,獲取某個作者的所有Posts,並且按照時間升序排列(在relations方法中,是按照時間降序),我們可以這樣做

  1. User::model()->with(array(  
  2.     'posts'=>array('order'=>'posts.create time ASC'),  
  3.     'profile',  
  4. ))->findAll(); 

動態選項也可以用在後裝載的方法中。在這裏,我們需要調用一個跟關係名同名的方法,向這個方法傳遞動態參數。例如,下例中返回user的狀態爲1的posts

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

1.7 關聯查詢執行

如此前所說的,我們在查詢關聯多個對象的時候,一般採用預裝載的方法。他生成了一個很長的語句,把所有用到的表都join起來。如果只是基於一個表字段的過濾,看起來是很合適的。但是,很多情況下他是很低效的。

考慮一種情況,我們想獲得最近的一些Posts,同時還有他們的Coments。假設每篇Post有10條Coments,用一長句的SQL,會帶來很多多餘的Post,然後這些多餘的Post還會很多coment。現在讓我們來嘗試另一種方法:我們先查詢最新的Posts,然後查詢他們的Coments。在這個新的方法中,我們需要執行2次SQL語句,好處是沒有多餘的結果出來。

問題來了,到底哪種方法纔是最佳的?這個是沒有決定的答案的。用一長句的SQL,對於底層的數據庫來說,解析與執行都會比較高效。但另一方面,用這樣單句SQL查詢出來的結果,有很多多餘的數據,要消耗時間來讀取處理他們。

出於這個原因,Yii提供了together的方法,讓我們選擇要用哪種方法。默認情況下,Yii採用預裝載的方式,生成一個單句的SQL, 希望在主模型中有LIMIT。我們可以再relation中,設置together的默認值爲true。這樣,就算LIMIT後,還是強制生成單句的SQL。設置together·爲false,會生成這些表的各自SQL。例如,想用分開的語句查詢最新的Posts以及他們的評論,我們可以在Post的relation中聲明comments:

  1. public function relations()  
  2. {  
  3.     return array(  
  4.         'comments' => array(self::HAS MANY, 'Comment''post id''together'=>false),  
  5.     );  

也可以在預裝載的調用中動態設置這個選項:

  1. $posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll(); 

1.8 靜態查詢

除了上述的關聯查詢,Yii也支持所謂的靜態查詢(聚集查詢)。這個涉及到訪問相關對象的聚集信息,例如每篇Post包含了多少comments,每個產品的平均級別等。靜態查詢只有在HAS_MANY(post has many coments)或者是MANY_MANY(e.g. a post belongs to many categories and a category has many posts).的關係中才適用。

執行靜態查詢跟我們之前說的關聯查詢非常的相似。首先,我們得像關聯查詢一樣的,在CActiveRecord中的relation方法中聲明這個靜態查詢。

  1. class Post extends CActiveRecord  
  2. {  
  3.     public function relations()  
  4.     {     
  5.         return array(  
  6.             'commentCount'=>array(self::STAT, 'Comment''post_id'),  
  7.             'categoryCount'=>array(self::STAT, 'Category''post category(post_id, categoryid)'),  
  8.         );  
  9.     }  

在上面的代碼中,我們聲明瞭兩個靜態查詢:commentCount,統計這篇文章的評論總數,categoryCount統計這篇文章所屬分類的總數。請注意,Post跟Coment的關係是HAS_ONE, 跟Category的關係是MANY_MANY。可以看出,這個說明跟我們上一章節中所看到的非常相似。唯一的區別在於關係類型是STAT。

在上面的聲明之後,我們可以通過調用$post->commentCount來獲得這篇文章的評論數。當我們第一次調用這個屬性的時候,一個SQL語句會被隱式的執行來獲取這個結果。我們已經知道了,這種所謂的後裝載方法。如果我們想獲得很多文章的評論數,也可以用預裝載的方式:

  1. $posts=Post::model()->with('commentCount''categoryCount')->findAll(); 

上述語句將會執行3條SQL語句,返回posts,以及他的評論數,所屬分類數。如果用後裝載的方式,如果有N條Post記錄,我們要執行2*N+1的SQL語句來得到。

默認情況下,一個靜態查詢會計算統計數(如上例中的評論數以及分類數)。我們可以在relations()方法中定製,可用選項統計如下:

  1. ● select: the statistical expression. Defaults to COUNT(*), meaning the count of child objects.  
  2. 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.  
  3. condition: the WHERE clause. It defaults to emptyempty.  
  4. params: the parameters to be bound to the generated SQL statement. This should  
  5. be given as an array of name-value pairs.  
  6. order: the ORDER BY clause. It defaults to emptyempty.  
  7. group: the GROUP BY clause. It defaults to emptyempty.  
  8. having: the HAVING clause. It defaults to emptyempty

1.9 用命名空間來關聯查詢

關聯查詢也可以結合命名空間來實現,有兩種實現方式。第一種,命名空間是在主模型中,另外一種,命名空間在所關聯的模型中。

下面的代碼演示如何在主模型應用命名空間:

  1. $posts=Post::model()->published()->recently()->with('comments')->findAll();  

這非常像非關聯查詢,是吧?唯一的區別在於,在命名空間鏈後面,我們有with的方法。這條語句將返回最近發佈的post以及他們的coments。

另外,下面的代碼演示如何在被關聯的模型應用命名空間。

  1. $posts=Post::model()->with('comments:recently:approved')->findAll();  
  2. // or since 1.1.7  
  3. $posts=Post::model()->with(array(  
  4.     'comments'=>array(  
  5.         'scopes'=>array('recently','approved')  
  6.     ),  
  7. ))->findAll();  
  8. // or since 1.1.7  
  9. $posts=Post::model()->findAll(array(  
  10.     'with'=>array(  
  11.         'comments'=>array(  
  12.             'scopes'=>array('recently','approved')  
  13.         ),  
  14.     ),  
  15. )); 

上面的代碼會返回Posts,以及他們的被通過的評論。注意到,coments是被關聯的名稱,而recently, approved是在coments裏聲明的命名空間。他們之間要用冒號分割。

命名空間也可以在CActiveRecord::relations()的規則中,用with選項來聲明。下面的例子中,如果我們調用$user->posts, 他會返回posts的所有驗證通過的評論。

 

  1. class User extends CActiveRecord  
  2. {  
  3.     public function relations()  
  4.     {  
  5.         return array(  
  6.             'posts'=>array(self::HAS MANY, 'Post''author id',  
  7.             'with'=>'comments:approved'),  
  8.         );  
  9.     }  
  10. }  
  11. // or since 1.1.7  
  12. class User extends CActiveRecord  
  13. {  
  14.     public function relations()  
  15.     {  
  16.         return array(  
  17.             'posts'=>array(self::HAS MANY, 'Post''author id',  
  18.                 'with'=>array(  
  19.                     'comments'=>array(  
  20.                         'scopes'=>'approved' 
  21.                     ),  
  22.                 ),  
  23.             ),  
  24.         );  
  25.     }  

從1.1.7開始,yii可以向關聯的命名空間傳遞參數。例如,你在Post模型中,有一個叫做rated的命名空間,用來規定只接受規定級別的文章,可以在User中調用:

  1. $users=User::model()->findAll(array(  
  2.     'with'=>array(  
  3.         'posts'=>array(  
  4.             'scopes'=>array(  
  5.                 'rated'=>5,  
  6.             ),  
  7.         ),  
  8.     ),  
  9. )); 

1.10 通過through關聯查詢

用through的時候,關係定義如下:

  1. 'comments'=>array(self::HAS MANY,'Comment',array('key1'=>'key2'),'through'=>'posts'), 

在上面代碼中的array('key1'=>'key2'):

key1:through中的relation定義的key(本例中的posts)

key2:關聯模型的key(本例中的coment)

through可以用在HAS_ONE和HAS_MANY的關係中。

一個例子表明HAS_MANY的是,當用戶通過角色分組是,選出特定組的用戶。更復雜一點的例子,獲取特定組用戶的所有評論。下面的例子中,我們將示範在單一模型中,如何用through關聯多種關係。

  1. class Group extends CActiveRecord  
  2. {  
  3.     ...  
  4.     public function relations()  
  5.     {  
  6.         return array(  
  7.             'roles'=>array(self::HAS MANY,'Role','group id'),  
  8.             'users'=>array(self::HAS MANY,'User',array('user id'=>'id'),'through'=>'roles'),  
  9.             'comments'=>array(self::HAS_MANY,'Comment',array('id'=>'user id'),'through'=>'users'),  
  10.         );  
  11.     }  
  12. }  
  13.  
  14. // get all groups with all corresponding users  
  15. $groups=Group::model()->with('users')->findAll();  
  16. // get all groups with all corresponding users and roles  
  17. $groups=Group::model()->with('roles','users')->findAll();  
  18. // get all users and roles where group ID is 1  
  19. $group=Group::model()->findByPk(1);  
  20. $users=$group->users;  
  21. $roles=$group->roles;  
  22. // get all comments where group ID is 1  
  23. $group=Group::model()->findByPk(1);  
  24. $comments=$group->comments; 

 

HAS_ONE的例子,例如,通過through,來調用user綁定的配置資料中的address。所有這些實體都有對應的模型。

  1. class User extends CActiveRecord  
  2. {  
  3.     ...  
  4.     public function relations()  
  5.     {  
  6.         return array(  
  7.             'profile'=>array(self::HAS_ONE,'Profile','user id'),  
  8.             'address'=>array(self::HAS_ONE,'Address',array('id'=>'profile_id'),'through'=>'profile'),  
  9.         );  
  10.     }  
  11.  
  12. // get address of a user whose ID is 1
  13. $user=User::model()->findByPk(1);
    $address=$user->address;

through 自己

through可以通過綁定一個橋接模型,用於 自身。在本例中是一個用戶作爲另一個用戶的導師

 

我們按照以下的方法聲明他們的關係:

  1. class User extends CActiveRecord  
  2. {  
  3.     ...  
  4.     public function relations()  
  5.     {  
  6.         return array(  
  7.             'mentorships'=>array(self::HAS_MANY,'Mentorship','teacher_id','joinType'=>'INNER JOIN'),  
  8.             'students'=>array(self::HAS_MANY,'User',array('student_id'=>'id'),'through'=>'mentorships','joinType'=>'INNER JOIN'),  
  9.         );  
  10.     }  
  11. // get all students taught by teacher whose ID is 1
    $teacher=User::model()->findByPk(1);
    $students=$teacher->students;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章