模型的關(guān)聯(lián)操作是模型的最為強(qiáng)大,也是最為復(fù)雜的部分,通過(guò)模型關(guān)聯(lián)操作把數(shù)據(jù)表的關(guān)聯(lián)關(guān)系對(duì)象化,解決了大部分常用的關(guān)聯(lián)場(chǎng)景,封裝的關(guān)聯(lián)操作比起常規(guī)的數(shù)據(jù)庫(kù)聯(lián)表操作更加智能和高效,并且直觀,所以關(guān)聯(lián)也可以說(shuō)是模型的一個(gè)殺手锏,一旦使用了就會(huì)越來(lái)越喜歡,本章學(xué)習(xí)的內(nèi)容包括:
- 定義關(guān)聯(lián)
- 基礎(chǔ)方法
- 關(guān)聯(lián)查詢
- 關(guān)聯(lián)自定義查詢
- 關(guān)聯(lián)約束
- 關(guān)聯(lián)預(yù)載入
- 關(guān)聯(lián)統(tǒng)計(jì)
- 關(guān)聯(lián)輸出
- [關(guān)聯(lián)實(shí)例])
- 總結(jié)
要掌握關(guān)聯(lián),最關(guān)鍵是要掌握如何定義關(guān)聯(lián)(包括明確模型之間的關(guān)聯(lián)關(guān)系)以及如何進(jìn)行關(guān)聯(lián)查詢,其它的關(guān)聯(lián)寫入操作基本了解即可,因?yàn)槟憧梢赃x擇采用其它的替代方案完成區(qū)別并不大(對(duì)于多對(duì)多關(guān)聯(lián),關(guān)聯(lián)寫入的優(yōu)勢(shì)才能體現(xiàn)出來(lái)),也充分說(shuō)明了關(guān)聯(lián)的優(yōu)勢(shì)主要在查詢_
定義關(guān)聯(lián)
定義關(guān)聯(lián)最主要是要搞清楚模型之間的關(guān)聯(lián)關(guān)系是什么,然后才能“對(duì)癥下藥”調(diào)用相關(guān)的關(guān)聯(lián)方法。
我們先舉個(gè)簡(jiǎn)單的例子來(lái)了解下關(guān)聯(lián)關(guān)系的概念,例如有一個(gè)多用戶博客系統(tǒng),這個(gè)系統(tǒng)可能包括下面的一些數(shù)據(jù)表(當(dāng)然實(shí)際上可能遠(yuǎn)遠(yuǎn)不止這些表,只是用來(lái)說(shuō)明一些典型問(wèn)題和僅供參考):城市表(city)、用戶表(user)、博客表(blog ,只記錄博客基礎(chǔ)信息)、內(nèi)容表(content ,記錄博客的具體內(nèi)容和擴(kuò)展信息)、分類表(cate)、評(píng)論表(comment)、角色表(role)和用戶-角色表(auth)。
關(guān)聯(lián)關(guān)系通常有一個(gè)參照模型,這個(gè)參照模型我們一般稱為主模型(或者當(dāng)前模型),關(guān)聯(lián)關(guān)系對(duì)應(yīng)的模型就是關(guān)聯(lián)模型,關(guān)聯(lián)關(guān)系是指定義在主模型中的關(guān)聯(lián),有些關(guān)聯(lián)關(guān)系還會(huì)設(shè)計(jì)到一個(gè)中間表的概念,但中間表不一定需要存在具體的模型。
主模型和關(guān)聯(lián)模型之間通常是通過(guò)某個(gè)外鍵進(jìn)行關(guān)聯(lián),而這個(gè)外鍵的命名系統(tǒng)會(huì)有一個(gè)約定規(guī)則,通常是主模型名稱+
_id,盡量遵循這個(gè)約定會(huì)給關(guān)聯(lián)定義帶來(lái)很大簡(jiǎn)化。
假設(shè)我們已經(jīng)給這些數(shù)據(jù)表創(chuàng)建了各自的模型,這些模型之間存在一定的關(guān)聯(lián)關(guān)系,我們來(lái)分析下(注意關(guān)聯(lián)關(guān)系是相對(duì)某個(gè)參照模型的):
- 博客和內(nèi)容是一對(duì)一的,屬于
hasOne關(guān)聯(lián)(以博客模型為參照),一般content表會(huì)有一個(gè)blog_id字段; - 反過(guò)來(lái)內(nèi)容和博客之間就屬于
belongsTo關(guān)聯(lián)(以內(nèi)容模型為參照); - 博客一定屬于某個(gè)分類(這里設(shè)計(jì)為單個(gè)分類),就是
belongsTo關(guān)聯(lián)(以博客模型為參照),一般blog表會(huì)有一個(gè)cate_id字段; - 而每個(gè)分類下面有多個(gè)博客,因此屬于
hasMany關(guān)聯(lián)(以分類模型為參照); - 每個(gè)用戶會(huì)發(fā)布多個(gè)博客,所以用戶和博客之間屬于
hasMany關(guān)聯(lián)(以用戶模型為參照),一般blog表會(huì)有一個(gè)user_id字段; - 每個(gè)博客會(huì)有多個(gè)評(píng)論,所以博客和評(píng)論之間屬于
hasMany關(guān)聯(lián)(以博客模型為參照); - 每個(gè)用戶可以有多個(gè)角色,而每個(gè)角色也會(huì)有多個(gè)用戶,因此用戶和角色屬于
belongsToMany關(guān)聯(lián)(多對(duì)多關(guān)聯(lián)無(wú)論以哪個(gè)模型為參照關(guān)聯(lián)不變),用戶和角色之間的中間表就是用戶權(quán)限表,這個(gè)中間表通常會(huì)設(shè)計(jì)user_id和role_id字段; - 每個(gè)城市有多個(gè)用戶,而每個(gè)用戶有多個(gè)博客,城市和博客之間并無(wú)直接關(guān)系,而是通過(guò)中間模型產(chǎn)生關(guān)聯(lián),城市和博客之間就屬于
hasManyThrough關(guān)聯(lián)(遠(yuǎn)程一對(duì)多,以城市模型為參照),中間模型就是用戶; - 如果針對(duì)某個(gè)用戶和某個(gè)博客都能發(fā)表評(píng)論,那么用戶、博客和評(píng)論之間就形成了一種多態(tài)一對(duì)多的關(guān)聯(lián)關(guān)系,也就是說(shuō)用戶會(huì)有多個(gè)評(píng)論(
morphMany關(guān)聯(lián),以用戶模型為參照),博客會(huì)有多個(gè)評(píng)論(morphMany關(guān)聯(lián),以博客模型為參照),但評(píng)論表只有一個(gè),評(píng)論表對(duì)于博客和用戶來(lái)說(shuō),不需要定義兩個(gè)關(guān)聯(lián)關(guān)系,而只需要定義一個(gè)morphTo關(guān)聯(lián)(以評(píng)論模型為參照)即可,評(píng)論表的設(shè)計(jì)就會(huì)被改造以滿足多態(tài)的設(shè)計(jì),普遍的設(shè)計(jì)是會(huì)增加一個(gè)多態(tài)類型的字段來(lái)標(biāo)識(shí)屬于某個(gè)類型(這里就是用戶或者博客類型);
大概了解了關(guān)聯(lián)關(guān)系的概念后,我們來(lái)看下關(guān)聯(lián)的表現(xiàn)方式是怎樣的。從面向?qū)ο蟮慕嵌葋?lái)看關(guān)聯(lián)的話,模型的關(guān)聯(lián)其實(shí)應(yīng)該是模型的某個(gè)屬性,比如用戶的檔案關(guān)聯(lián),就應(yīng)該是下面的情況:
// 用戶的檔案
$user->profile;
// 用戶的檔案屬性中的手機(jī)資料
$user->profile->mobile;
$user本身是一個(gè)User模型的對(duì)象實(shí)例,而$user->profile則是一個(gè)Profile模型的對(duì)象實(shí)例,所以具備模型的所有特性而不是一個(gè)數(shù)組,包括進(jìn)行Profile模型的CURD操作和業(yè)務(wù)邏輯執(zhí)行,$user->profile->mobile則表示獲取Profile模型對(duì)象實(shí)例的mobile數(shù)據(jù),包括下面的操作也是有效的。
// 對(duì)查詢出來(lái)的關(guān)聯(lián)模型進(jìn)行數(shù)據(jù)更新
$user->profile->email = 'thinkphp@qq.com'
$user->profile->save();
這種關(guān)聯(lián)關(guān)系使用Db類是無(wú)法完成的,所以這個(gè)使命是由模型來(lái)完成的,模型的關(guān)聯(lián)用法很好的解決了關(guān)聯(lián)的對(duì)象化,支持大部分的關(guān)聯(lián)場(chǎng)景和需求。
為了更方便和靈活的定義模型的關(guān)聯(lián)關(guān)系,框架選擇了方法定義而不是屬性定義的方式,每個(gè)關(guān)聯(lián)屬性其實(shí)是對(duì)應(yīng)了一個(gè)模型的關(guān)聯(lián)方法,這個(gè)關(guān)聯(lián)屬性和模型的數(shù)據(jù)一樣是動(dòng)態(tài)的,并非模型類的實(shí)際屬性,下面我們會(huì)來(lái)解釋下原理。
例如上面的關(guān)聯(lián)屬性就是在User模型類中定義了一個(gè)profile方法:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
public function profile()
{
return $this->hasOne('Profile');
}
}
當(dāng)我們?cè)L問(wèn)User模型對(duì)象實(shí)例的profile屬性的時(shí)候,其實(shí)就是調(diào)用了profile方法來(lái)完成關(guān)聯(lián)查詢。我們知道當(dāng)獲取一個(gè)模型的屬性的時(shí)候會(huì)觸發(fā)模型的獲取器,而當(dāng)獲取器在沒(méi)有檢測(cè)到模型有對(duì)應(yīng)屬性的時(shí)候就會(huì)檢查是否存在關(guān)聯(lián)方法定義(對(duì)于關(guān)聯(lián)方法的判斷很簡(jiǎn)單,關(guān)聯(lián)方法返回的是一個(gè)think\model\Relation對(duì)象),如果存在則調(diào)用對(duì)應(yīng)關(guān)聯(lián)類的getRelation方法。
我們知道模型的方法名都是駝峰命名的,所以系統(tǒng)做了一個(gè)兼容處理,當(dāng)我們定義了一個(gè)userProfile的關(guān)聯(lián)方法的時(shí)候,在獲取關(guān)聯(lián)屬性的時(shí)候,下面兩種方式都是有效的:
$user->userProfile;
$user->user_profile;
我們推薦關(guān)聯(lián)屬性統(tǒng)一使用后者,和數(shù)據(jù)表的字段命名規(guī)范一致,因此在很多時(shí)候系統(tǒng)自動(dòng)獲取關(guān)聯(lián)屬性的時(shí)候采用的也是后者。
有興趣的可以去了解下Model類中getAttr方法的源碼,看看關(guān)聯(lián)屬性獲取的具體代碼實(shí)現(xiàn)。
看起來(lái)很普通的一個(gè)方法賦予了模型神奇的關(guān)聯(lián)特性,一個(gè)小小的hasOne方法背后是強(qiáng)大而復(fù)雜的關(guān)聯(lián)實(shí)現(xiàn)邏輯(后面會(huì)慢慢給你描述),ThinkPHP所說(shuō)的讓開發(fā)更簡(jiǎn)單就是因?yàn)橛斜姸噙@些簡(jiǎn)單而又神奇的特性。
關(guān)聯(lián)方法的定義最關(guān)鍵是要搞清楚具體應(yīng)該使用何種關(guān)聯(lián)關(guān)系,其次是掌握不同的關(guān)聯(lián)關(guān)系的定義方法和參數(shù)。
可以簡(jiǎn)單的理解為關(guān)聯(lián)定義就是在模型類中添加一個(gè)方法(該方法注意不要和模型的對(duì)象屬性以及其它業(yè)務(wù)邏輯方法沖突),一般情況下無(wú)需任何參數(shù),并在方法中指定一種關(guān)聯(lián)關(guān)系,比如上面的hasOne關(guān)聯(lián)關(guān)系(關(guān)聯(lián)的玄妙和復(fù)雜就在這個(gè)關(guān)聯(lián)方法的定義),5.0版本支持的關(guān)聯(lián)關(guān)系包括下面七種,后面會(huì)給大家陸續(xù)介紹:
| 模型方法 | 關(guān)聯(lián)類型 |
|---|---|
hasOne |
一對(duì)一HAS ONE |
belongsTo |
一對(duì)一BELONGS TO |
hasMany |
一對(duì)多 HAS MANY |
hasManyThrough |
遠(yuǎn)程一對(duì)多 HAS MANY THROUTH |
belongsToMany |
多對(duì)多 BELONGS TO MANY |
morphMany |
多態(tài)一對(duì)多 MORPH MANY |
morphTo |
多態(tài) MORPH TO |
關(guān)聯(lián)方法的第一個(gè)參數(shù)就是要關(guān)聯(lián)的模型名稱,也就是說(shuō)當(dāng)前模型的關(guān)聯(lián)模型必須也是已經(jīng)定義的一個(gè)模型。
一般不需要使用命名空間,會(huì)自動(dòng)使用當(dāng)前模型的命名空間,如果不同請(qǐng)使用完整命名空間定義,例如:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
public function profile()
{
// Profile模型和當(dāng)前模型的命名空間不一致
return $this->hasOne('app\model\Profile');
}
}
兩個(gè)模型之間因?yàn)閰⒄漳P偷牟煌蜁?huì)產(chǎn)生相對(duì)的但不一定相同的關(guān)聯(lián)關(guān)系,并且相對(duì)的關(guān)聯(lián)關(guān)系只有在需要調(diào)用的時(shí)候才需要定義,下面是每個(gè)關(guān)聯(lián)類型的相對(duì)關(guān)聯(lián)關(guān)系對(duì)照:
| 類型 | 關(guān)聯(lián)關(guān)系 | 相對(duì)的關(guān)聯(lián)關(guān)系 |
|---|---|---|
| 一對(duì)一 | hasOne |
belongsTo |
| 一對(duì)多 | hasMany |
belongsTo |
| 多對(duì)多 | belongsToMany |
belongsToMany |
| 遠(yuǎn)程一對(duì)多 | hasManyThrough |
不支持 |
| 多態(tài)一對(duì)多 | morphMany |
morphTo |
除此之外,關(guān)聯(lián)定義的幾個(gè)要點(diǎn)必須了解:
- 關(guān)聯(lián)方法必須使用駝峰法命名;
- 關(guān)聯(lián)方法一般無(wú)需定義任何參數(shù);
- 關(guān)聯(lián)調(diào)用的時(shí)候駝峰法和小寫+下劃線都支持;
- 關(guān)聯(lián)字段設(shè)計(jì)盡可能按照規(guī)范可以簡(jiǎn)化關(guān)聯(lián)定義;
- 關(guān)聯(lián)方法定義可以添加額外查詢條件;
關(guān)聯(lián)方法定義參數(shù)說(shuō)明:
下面先對(duì)七種關(guān)聯(lián)關(guān)系的定義方法及參數(shù)給出一個(gè)大致的說(shuō)明。
hasOne關(guān)聯(lián)
用法:hasOne('關(guān)聯(lián)模型','外鍵','主鍵');
除了關(guān)聯(lián)模型外,其它參數(shù)都是可選。
- 關(guān)聯(lián)模型(必須):模型名或者模型類名
-
外鍵:默認(rèn)的外鍵規(guī)則是當(dāng)前模型名(不含命名空間,下同)+
_id,例如user_id - 主鍵:當(dāng)前模型主鍵,一般會(huì)自動(dòng)獲取也可以指定傳入
belongsTo關(guān)聯(lián)
用法:belongsTo('關(guān)聯(lián)模型','外鍵','關(guān)聯(lián)表主鍵');
除了關(guān)聯(lián)模型外,其它參數(shù)都是可選。
- 關(guān)聯(lián)模型(必須):模型名或者模型類名
-
外鍵:當(dāng)前模型外鍵,默認(rèn)的外鍵名規(guī)則是關(guān)聯(lián)模型名+
_id - 關(guān)聯(lián)主鍵:關(guān)聯(lián)模型主鍵,一般會(huì)自動(dòng)獲取也可以指定傳入
hasMany關(guān)聯(lián)
用法:hasMany('關(guān)聯(lián)模型','外鍵','主鍵');
除了關(guān)聯(lián)模型外,其它參數(shù)都是可選。
- 關(guān)聯(lián)模型(必須):模型名或者模型類名
-
外鍵:關(guān)聯(lián)模型外鍵,默認(rèn)的外鍵名規(guī)則是當(dāng)前模型名+
_id - 主鍵:當(dāng)前模型主鍵,一般會(huì)自動(dòng)獲取也可以指定傳入
hasManyThrough
用法:hasManyThrough('關(guān)聯(lián)模型','中間模型','外鍵','中間表關(guān)聯(lián)鍵','主鍵');
- 關(guān)聯(lián)模型(必須):模型名或者模型類名
- 中間模型(必須):模型名或者模型類名
-
外鍵:默認(rèn)的外鍵名規(guī)則是當(dāng)前模型名+
_id -
中間表關(guān)聯(lián)鍵:默認(rèn)的中間表關(guān)聯(lián)鍵名的規(guī)則是中間模型名+
_id - 主鍵:當(dāng)前模型主鍵,一般會(huì)自動(dòng)獲取也可以指定傳入
belongsToMany關(guān)聯(lián)
用法:belongsToMany('關(guān)聯(lián)模型','中間表','外鍵','關(guān)聯(lián)鍵');
- 關(guān)聯(lián)模型(必須):模型名或者模型類名
-
中間表:默認(rèn)規(guī)則是當(dāng)前模型名+
_+關(guān)聯(lián)模型名 (注意,在V5.0.8版本之前需要添加表前綴) -
外鍵:中間表的當(dāng)前模型外鍵,默認(rèn)的外鍵名規(guī)則是關(guān)聯(lián)模型名+
_id -
關(guān)聯(lián)鍵:中間表的當(dāng)前模型關(guān)聯(lián)鍵名,默認(rèn)規(guī)則是當(dāng)前模型名+
_id
morphMany關(guān)聯(lián)
用法:morphMany('關(guān)聯(lián)模型','多態(tài)字段','多態(tài)類型');
- 關(guān)聯(lián)模型(必須):模型名或者模型類名
- 多態(tài)字段:多態(tài)字段信息定義包含兩種方式,字符串的話表示多態(tài)字段的前綴,數(shù)組則表示實(shí)際的多態(tài)字段
- 多態(tài)類型:默認(rèn)是當(dāng)前模型名
數(shù)據(jù)表的多態(tài)字段一般包含兩個(gè)字段:多態(tài)類型和多態(tài)主鍵。
如果多態(tài)字段使用字符串例如morph,那么多態(tài)類型和多態(tài)主鍵字段分別對(duì)應(yīng)morph_type 和 morph_id,如果用數(shù)組方式定義的話,就改為['morph_type','morph_id']即可。
morphTo關(guān)聯(lián)
用法:morphTo('多態(tài)字段','多態(tài)類型別名(數(shù)組)');
-
多態(tài)字段:定義和
morphMany一致 - 多態(tài)類型別名:用于設(shè)置特殊的多態(tài)類型(比如用數(shù)字標(biāo)識(shí)的多態(tài)類型)
基礎(chǔ)方法
關(guān)聯(lián)操作經(jīng)常會(huì)涉及到幾個(gè)重要的方法,也是關(guān)聯(lián)操作的基礎(chǔ),掌握了這幾個(gè)方法對(duì)于掌握關(guān)聯(lián)(尤其是關(guān)聯(lián)查詢)有很大的幫助,包括:
| 方法名 | 作用 |
|---|---|
relation |
關(guān)聯(lián)查詢 |
with |
關(guān)聯(lián)預(yù)載入 |
withCount |
關(guān)聯(lián)統(tǒng)計(jì)(V5.0.5+) |
load |
關(guān)聯(lián)延遲預(yù)載入(V5.0.5+) |
together |
關(guān)聯(lián)自動(dòng)寫入(V5.0.5+) |
我們對(duì)這些方法先有個(gè)基本的了解,暫時(shí)不用深究,首先要明白的是如何使用這些方法。load方法是數(shù)據(jù)集對(duì)象的方法,together方法是模型類提供的方法,其它幾個(gè)都是Query類提供的鏈?zhǔn)椒椒?,在查詢方法之前調(diào)用。
relation和with方法的主要區(qū)別在于relation是單純的關(guān)聯(lián)查詢,比如你查詢一個(gè)用戶列表,然后需要關(guān)聯(lián)查詢用戶的檔案數(shù)據(jù),使用relation方法的話就是,我先查詢用戶列表數(shù)據(jù),然后每個(gè)每個(gè)用戶再單純查詢檔案數(shù)據(jù)。如果用戶列表數(shù)據(jù)有10個(gè),那么就會(huì)產(chǎn)生11次查詢。如果使用with方法的話,雖然最終查詢出來(lái)的關(guān)聯(lián)數(shù)據(jù)是一樣的,但由于with查詢使用的是預(yù)載入查詢,因此實(shí)際只會(huì)產(chǎn)生2次查詢。而load方法則更先進(jìn),先查詢出用戶列表,然后在需要關(guān)聯(lián)數(shù)據(jù)的時(shí)候使用load方法獲取關(guān)聯(lián)數(shù)據(jù),尤其適合動(dòng)態(tài)關(guān)聯(lián)的情況,最終也是兩次查詢,因此稱為延遲預(yù)載入。
由于模型關(guān)聯(lián)的對(duì)象化封裝機(jī)制的優(yōu)勢(shì),其實(shí)relation方法基本上很少被用到,而是使用關(guān)聯(lián)惰性查詢及關(guān)聯(lián)方法的自定義查詢來(lái)替代了(會(huì)在下一節(jié)給你講解)。最常用的莫過(guò)于with方法,因?yàn)樽畛S靡虼吮粌?nèi)置到模型類的get和all方法的第二個(gè)參數(shù)了,我們后面對(duì)with方法的用法說(shuō)明也均適用于get和all方法的第二個(gè)參數(shù)。withCount用于在不獲取關(guān)聯(lián)數(shù)據(jù)的情況下提供關(guān)聯(lián)數(shù)據(jù)的統(tǒng)計(jì),在查詢一對(duì)多或者多對(duì)多關(guān)聯(lián)的時(shí)候才需要使用。load方法則適用于在數(shù)據(jù)集的延遲預(yù)載入關(guān)聯(lián)查詢(對(duì)于默認(rèn)的數(shù)據(jù)集查詢類型系統(tǒng)提供了一個(gè)load_relation助手函數(shù),作用是等效的)。together方法用于一對(duì)一的關(guān)聯(lián)自動(dòng)寫入操作(包括新增、更新和刪除),提供了更簡(jiǎn)單的關(guān)聯(lián)寫入機(jī)制。
雖然作用不盡相同,但這幾個(gè)方法的使用方法都是類似的,這四個(gè)方法都只有一個(gè)參數(shù),參數(shù)類型包括字符串和數(shù)組,并且數(shù)組方式還支持索引數(shù)組以方便完成關(guān)聯(lián)的自定義查詢。
下面以
relation方法為例,來(lái)說(shuō)明下上述關(guān)聯(lián)方法的基本用法(我們演示的是查詢用法,至于代碼示例中的具體關(guān)聯(lián)是怎么定義的你暫時(shí)不必關(guān)注或者自行按照前面講解的關(guān)聯(lián)定義進(jìn)行測(cè)試定義),其它的幾個(gè)方法用法完全一樣,就不再一一重復(fù),后面具體涉及到的某個(gè)方法的時(shí)候可能只會(huì)采用其中一種或者個(gè)別進(jìn)行講解,請(qǐng)悉知。
最簡(jiǎn)單的用法是:
// 查詢用戶的Profile關(guān)聯(lián)數(shù)據(jù)
$users = $user->relation('profile')->select();
// 查詢用戶的Book關(guān)聯(lián)數(shù)據(jù)
$users = $user->relation('books')->select();
關(guān)聯(lián)查詢的方法返回的依然是包含User對(duì)象實(shí)例的數(shù)據(jù)集,relation方法設(shè)定的關(guān)聯(lián)查詢結(jié)果只是數(shù)據(jù)集中的User模型對(duì)象實(shí)例的某個(gè)關(guān)聯(lián)屬性。
relation方法傳入的字符串就是關(guān)聯(lián)定義的方法名而不是關(guān)聯(lián)模型的名稱,由于模型方法名使用的都是駝峰法規(guī)范,假設(shè)定義了一個(gè)名為userBooks的關(guān)聯(lián)方法的話,relation方法可以使用兩種方式的關(guān)聯(lián)查詢:
// 駝峰法的關(guān)聯(lián)方法定義
$users = $user->relation('userBooks')->select();
// 或者使用下面的方式等效
$users = $user->relation('user_books')->select();
第一種傳入的是實(shí)際的駝峰法關(guān)聯(lián)方法名userBooks,第二種是傳入小寫和下劃線的轉(zhuǎn)化名稱user_books,兩種關(guān)聯(lián)查詢用法都會(huì)實(shí)際定位到關(guān)聯(lián)方法名稱userBooks,所以關(guān)聯(lián)方法定義必須使用駝峰法。
對(duì)于上面的關(guān)聯(lián)查詢用法,在獲取關(guān)聯(lián)查詢數(shù)據(jù)的時(shí)候,同樣可以支持兩種方式:
foreach ($users as $user) {
dump($user->userBooks);
}
或者
foreach ($users as $user) {
dump($user->user_books);
}
默認(rèn)情況下,關(guān)聯(lián)方法獲取的是滿足關(guān)聯(lián)條件的所有數(shù)據(jù),如果需要自定義關(guān)聯(lián)查詢條件的話,可以使用
// 使用自定義關(guān)聯(lián)查詢
$user->relation(['books' => function ($query) {
$query->where('title', 'like', '%thinkphp%');
}])->select();
表示查詢?cè)撚脩魧懙臉?biāo)題中包含thinkphp的書籍,閉包中不僅僅可以使用查詢條件,還可以支持其它的鏈?zhǔn)椒椒ǎ热鐚?duì)關(guān)聯(lián)數(shù)據(jù)進(jìn)行排序和指定字段:
// 使用自定義關(guān)聯(lián)查詢
$user->relation(['books' => function ($query) {
$query
->field('id,name,title,pub_time,user_id')
->order('pub_time desc')
->whereTime('pub_time', 'year');
}])->select();
如果使用
field方法指定查詢字段,務(wù)必包含你的當(dāng)前模型的主鍵以及關(guān)聯(lián)模型的關(guān)鍵鍵,否則會(huì)導(dǎo)致關(guān)聯(lián)查詢失敗。
關(guān)聯(lián)方法可以同時(shí)指定多個(gè)關(guān)聯(lián),即使是不同的關(guān)聯(lián)類型,使用:
// 查詢用戶的Profile和Book關(guān)聯(lián)數(shù)據(jù)
$users = $user->relation('profile,books')->select();
下面的數(shù)組方式是等效的
// 查詢用戶的Profile和Book關(guān)聯(lián)數(shù)據(jù)
$users = $user->relation(['profile','books'])->select();
一般使用數(shù)組的話,主要需要使用閉包進(jìn)行自定義關(guān)聯(lián)查詢的情況,否則用逗號(hào)分割的字符串就可以了。
together方法不支持閉包,但可以支持?jǐn)?shù)組方式定義多個(gè)關(guān)聯(lián)方法
關(guān)聯(lián)查詢
在熟悉了如何定義關(guān)聯(lián)方法和關(guān)聯(lián)方法的基礎(chǔ)用法之后,我們來(lái)具體了解如何進(jìn)行實(shí)際的關(guān)聯(lián)查詢以及細(xì)節(jié)。
通常有兩種方式進(jìn)行關(guān)聯(lián)的數(shù)據(jù)獲?。宏P(guān)聯(lián)預(yù)查詢和關(guān)聯(lián)延遲查詢。
關(guān)聯(lián)預(yù)查詢方式就是使用上節(jié)提到的relation方法,使用
// 指定User模型的profile關(guān)聯(lián)
$user = User::relation('profile')->find(1);
// profile關(guān)聯(lián)屬性也是一個(gè)模型對(duì)象實(shí)例
dump($user->profile);
relation方法中傳入關(guān)聯(lián)(方法)名稱即可(多個(gè)可以使用逗號(hào)分割的字符串或者數(shù)組)。這種方式,無(wú)論你是否最終獲取profile屬性,都會(huì)事先進(jìn)行關(guān)聯(lián)查詢,因此稱為關(guān)聯(lián)預(yù)查詢。
如果關(guān)聯(lián)數(shù)據(jù)不存在,一對(duì)一關(guān)聯(lián)返回的是null,一對(duì)多關(guān)聯(lián)的話返回的是空數(shù)組或空數(shù)據(jù)集對(duì)象。
出于性能考慮,通常我們選擇關(guān)聯(lián)延遲查詢的方式。
// 不需要指定關(guān)聯(lián)
$user = User::get(1);
// 獲取profile屬性的時(shí)候自動(dòng)進(jìn)行關(guān)聯(lián)查詢
dump($user->profile);
這種方式下的關(guān)聯(lián)查詢是惰性的,只有在獲取關(guān)聯(lián)屬性的時(shí)候才會(huì)實(shí)際進(jìn)行關(guān)聯(lián)查詢,因此稱之為關(guān)聯(lián)延遲查詢。
關(guān)聯(lián)屬性的名稱一般就是關(guān)聯(lián)(定義)方法的名稱,但同時(shí)也支持駝峰關(guān)聯(lián)方法的小寫+下劃線轉(zhuǎn)化名稱。
關(guān)聯(lián)自定義查詢
模型的關(guān)聯(lián)方法除了會(huì)自動(dòng)在關(guān)聯(lián)獲取的時(shí)候自動(dòng)調(diào)用外,仍然可以作為查詢構(gòu)造器的鏈?zhǔn)讲僮鱽?lái)對(duì)待,以完成額外的附加條件或者其它自定義查詢(一對(duì)多的關(guān)聯(lián)關(guān)系時(shí)候比較多見類似場(chǎng)景),例如User模型定義了一個(gè)articles的hasMany關(guān)聯(lián):
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
public function articles()
{
return $this->hasMany('Article');
}
}
普通的關(guān)聯(lián)查詢獲取的是全部的關(guān)聯(lián)數(shù)據(jù),例如:
$user = User::get(1);
$articles = $user->articles;
articles返回的類型根據(jù)Article模型的數(shù)據(jù)集返回類型設(shè)定,如果Article模型返回的數(shù)據(jù)集類型是Collection,那么關(guān)聯(lián)數(shù)據(jù)集返回的也是Collection對(duì)象。
如果需要對(duì)關(guān)聯(lián)數(shù)據(jù)進(jìn)行篩選,例如需要查詢用戶發(fā)表的標(biāo)題里面包含think的文章,并且按照create_time倒序排序,則可以使用下面的方式:
$user = User::get(1);
$articles = $user->articles()
->where('title', 'like', '%think%')
->order('create_time desc')
->select();
調(diào)用articles()關(guān)聯(lián)方法的動(dòng)作有下面幾個(gè):
- 相當(dāng)于切換當(dāng)前模型到關(guān)聯(lián)模型對(duì)象(
Article); - 并且會(huì)自動(dòng)傳入關(guān)聯(lián)條件(
user_id = 1);
如果是一對(duì)多或者多對(duì)多關(guān)聯(lián),并且希望自主條件查詢關(guān)聯(lián)數(shù)據(jù)的話請(qǐng)參考該方式
如果你希望改變默認(rèn)的關(guān)聯(lián)查詢條件而不是在外部查詢的時(shí)候指定,可以直接在定義關(guān)聯(lián)的時(shí)候添加額外條件,例如上面的查詢條件可以寫成:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
public function articles()
{
return $this->hasMany('Article')
->where('title', 'like', '%think%')
->order('create_time desc');
}
}
關(guān)聯(lián)方法里面的查詢條件會(huì)自動(dòng)作為關(guān)聯(lián)查詢的條件帶入,下面的關(guān)聯(lián)查詢出來(lái)的數(shù)據(jù)就是包含額外條件的:
$user = User::get(1);
$articles = $user->articles;
如果需要你仍然可以在外部調(diào)用的時(shí)候追加額外條件,例如下面的關(guān)聯(lián)查詢就包含了關(guān)聯(lián)方法里面定義的和額外追加的條件:
$user = User::get(1);
$articles = $user->articles()
->where('name', 'thinkphp')
->field('id,name,title')
->select();
如果你擔(dān)心基礎(chǔ)的關(guān)聯(lián)條件定義影響你的其它查詢,你可以像下面一樣單獨(dú)定義多個(gè)關(guān)聯(lián)關(guān)系,各自獨(dú)立使用互不影響。
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
public function articles()
{
return $this->hasMany('Article');
}
public function articlesLike($title)
{
return $this->hasMany('Article')
->where('title', 'like', '%' . $title . '%')
->field('id,name,title')
->order('create_time desc');
}
}
articlesLike方法就作為自定義關(guān)聯(lián)查詢專用,并且需要傳入title參數(shù),用法如下:
$user = User::get(1);
$articles = $user->articlesLike('think')
->select();
下面的用法則是錯(cuò)誤的:
$user = User::get(1);
$articles = $user->articlesLike;
帶有參數(shù)的關(guān)聯(lián)定義方法不能直接用于關(guān)聯(lián)屬性獲取,只能用于鏈?zhǔn)疥P(guān)聯(lián)自定義查詢。
關(guān)聯(lián)約束
對(duì)于hasMany關(guān)聯(lián)關(guān)系,系統(tǒng)提供了根據(jù)關(guān)聯(lián)數(shù)據(jù)條件來(lái)查詢當(dāng)前模型數(shù)據(jù)的關(guān)聯(lián)約束方法,包括has和hasWhere兩個(gè)方法。
has方法主要用于查詢關(guān)聯(lián)數(shù)據(jù)的記錄數(shù)來(lái)作為當(dāng)前模型的查詢依據(jù),默認(rèn)是存在一條數(shù)據(jù)即可。
// 查詢有評(píng)論數(shù)據(jù)的文章
$list = Article::has('comments')->select();
可以指定關(guān)聯(lián)數(shù)據(jù)的數(shù)量進(jìn)行查詢,例如:
// 查詢?cè)u(píng)論超過(guò)3個(gè)的文章
$list = Article::has('comments', '>', 3)->select();
has方法的第二個(gè)參數(shù)支持>、>=、<、<= 以及 =,第三個(gè)參數(shù)是一個(gè)整數(shù)。
如果需要復(fù)雜的關(guān)聯(lián)查詢約束條件的話,可以使用hasWhere方法,例如:
// 查詢?cè)u(píng)論狀態(tài)正常的文章
$list = Article::hasWhere('comments', ['status' => 1])->select();
或者直接使用閉包查詢,然后在閉包里面使用鏈?zhǔn)椒椒ú樵儯?/p>
// 查詢最近一周包含think字符的評(píng)論的文章
$list = Article::hasWhere('comments', function ($query) {
$query
->whereTime('create_time', 'week')
->where('content', 'like', '%think%');
})->select();
使用閉包方式查詢的時(shí)候,需要注意一點(diǎn),如果查詢的關(guān)聯(lián)模型字段可能同時(shí)存在當(dāng)前模型和關(guān)聯(lián)模型的話,需要加上關(guān)聯(lián)模型的名稱作為別名。
// 查詢最近一周包含think字符的評(píng)論的文章
$list = Article::hasWhere('comments', function ($query) {
$query
->whereTime('Comment.create_time', 'week')
->where('content', 'like', '%think%');
})->select();
V5.0.5+版本開始,has也支持hasWhere的所有用法。
關(guān)聯(lián)預(yù)載入
關(guān)聯(lián)查詢只是為了方便,但在實(shí)際的應(yīng)用過(guò)程中,查詢多個(gè)數(shù)據(jù)的情況下如果數(shù)據(jù)較多,關(guān)聯(lián)查詢產(chǎn)生的性能開銷會(huì)較大(雖然這個(gè)很正常),比如查詢用戶的Profile關(guān)聯(lián)數(shù)據(jù)的話,如果有100個(gè)用戶數(shù)據(jù),就會(huì)產(chǎn)生100+1次查詢,這就是N+1查詢問(wèn)題,關(guān)聯(lián)預(yù)載入功能提供了更好的性能,但完成了一樣的關(guān)聯(lián)查詢效果。
關(guān)聯(lián)查詢的預(yù)查詢載入功能,主要解決了N+1次查詢的問(wèn)題,例如下面的查詢?nèi)绻?個(gè)記錄,會(huì)執(zhí)行4次查詢:
$list = User::all([1, 2, 3]);
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的profile模型數(shù)據(jù)
dump($user->profile);
}
如果使用關(guān)聯(lián)預(yù)查詢功能,對(duì)于一對(duì)一關(guān)聯(lián)來(lái)說(shuō),默認(rèn)只有一次查詢,對(duì)于一對(duì)多關(guān)聯(lián)的話,就變成2次查詢,有效提高性能,關(guān)聯(lián)預(yù)載入使用with方法指定需要預(yù)載入的關(guān)聯(lián)(方法),用法和relation方法類似。
$list = User::with('profile')->select([1, 2, 3]);
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的profile模型數(shù)據(jù)
dump($user->profile);
}
關(guān)聯(lián)的預(yù)載入查詢不是惰性的,是連同數(shù)據(jù)查詢一起完成的,但由于封裝的合并查詢,性能方面遠(yuǎn)遠(yuǎn)優(yōu)于普通的關(guān)聯(lián)惰性查詢,所以整體的查詢性能是非常樂(lè)觀的。
鑒于預(yù)載入查詢的重要性,模型的get和all方法的第二個(gè)參數(shù)可以直接傳入預(yù)載入?yún)?shù),例如下面的預(yù)載入查詢和前面是等效的:
$list = User::all([1, 2, 3], 'profile');
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的profile模型數(shù)據(jù)
dump($user->profile);
}
嵌套預(yù)載入
嵌套預(yù)載入指的是如果關(guān)聯(lián)模型本身還需要進(jìn)行關(guān)聯(lián)預(yù)載入的話,可以在當(dāng)前模型預(yù)載入查詢的時(shí)候直接指定,理論上嵌套是可以任意級(jí)別的(但實(shí)際上估計(jì)不會(huì)有這么復(fù)雜的關(guān)聯(lián)設(shè)計(jì)),假設(shè)Profile模型還關(guān)聯(lián)了一個(gè)名片模型(cards關(guān)聯(lián)方法),可以這樣進(jìn)行嵌套預(yù)載入查詢。
$list = User::all([1, 2, 3], 'profile.cards');
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)數(shù)據(jù)
dump($user->profile->cards);
}
一對(duì)一關(guān)聯(lián)的JOIN方式不支持嵌套預(yù)載入
預(yù)載入條件限制
可以在預(yù)載入的時(shí)候通過(guò)閉包指定額外的條件限制,但記住了,不要在閉包里面執(zhí)行任何的查詢,例如:
$list = User::with(['articles' => function ($query) {
$query->where('title', 'like', '%think%')
->field('id,name,title')
->order('create_time desc');
}])->select([1, 2, 3]);
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的profile模型數(shù)據(jù)
dump($user->profile);
}
如果是一對(duì)一預(yù)載入查詢的條件限制,注意
field方法要改為withField方法,否則會(huì)產(chǎn)生字段混淆。
延遲預(yù)載入
有些情況下,需要根據(jù)查詢出來(lái)的數(shù)據(jù)來(lái)決定是否需要使用關(guān)聯(lián)預(yù)載入,當(dāng)然關(guān)聯(lián)查詢本身就能解決這個(gè)問(wèn)題,因?yàn)殛P(guān)聯(lián)查詢是惰性的,不過(guò)用預(yù)載入的理由也很明顯,性能具有優(yōu)勢(shì)。
延遲預(yù)載入僅針對(duì)多個(gè)數(shù)據(jù)的查詢,因?yàn)閱蝹€(gè)數(shù)據(jù)的查詢用延遲預(yù)載入和關(guān)聯(lián)惰性查詢沒(méi)有任何區(qū)別,所以不需要使用延遲預(yù)載入。
如果你的數(shù)據(jù)集查詢返回的是數(shù)據(jù)集對(duì)象,可以使用調(diào)用數(shù)據(jù)集對(duì)象的load實(shí)現(xiàn)延遲預(yù)載入:
// 查詢數(shù)據(jù)集
$list = User::all([1, 2, 3]);
// 延遲預(yù)載入
$list->load('cards');
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的card模型數(shù)據(jù)
dump($user->cards);
}
如果你的數(shù)據(jù)集查詢返回的是數(shù)組,系統(tǒng)提供了一個(gè)load_relation助手函數(shù)可以完成同樣的功能。
// 查詢數(shù)據(jù)集
$list = User::all([1, 2, 3]);
// 延遲預(yù)載入
$list = load_relation($list, 'cards');
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的card模型數(shù)據(jù)
dump($user->cards);
}
關(guān)聯(lián)統(tǒng)計(jì)
有些時(shí)候,并不需要獲取關(guān)聯(lián)數(shù)據(jù),而只是希望獲取關(guān)聯(lián)數(shù)據(jù)的統(tǒng)計(jì)(關(guān)聯(lián)統(tǒng)計(jì)僅針對(duì)一對(duì)多或者多對(duì)多的關(guān)聯(lián)關(guān)系),這個(gè)時(shí)候可以使用withCount方法進(jìn)行制定關(guān)聯(lián)的統(tǒng)計(jì)。
$list = User::withCount('cards')->select([1, 2, 3]);
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的card關(guān)聯(lián)統(tǒng)計(jì)
echo $user->cards_count;
}
關(guān)聯(lián)統(tǒng)計(jì)功能會(huì)在模型的對(duì)象屬性中自動(dòng)添加一個(gè)以“關(guān)聯(lián)方法名+_count”為名稱的動(dòng)態(tài)屬性來(lái)保存相關(guān)的關(guān)聯(lián)統(tǒng)計(jì)數(shù)據(jù)。
如果需要對(duì)關(guān)聯(lián)統(tǒng)計(jì)進(jìn)行條件過(guò)濾,可以使用
$list = User::withCount(['cards' => function ($query) {
$query->where('status', 1);
}])->select([1, 2, 3]);
foreach ($list as $user) {
// 獲取用戶關(guān)聯(lián)的card關(guān)聯(lián)統(tǒng)計(jì)
echo $user->cards_count;
}
一對(duì)一關(guān)聯(lián)關(guān)系使用關(guān)聯(lián)統(tǒng)計(jì)是無(wú)效的,一般可以用
exists查詢來(lái)判斷是否存在關(guān)聯(lián)數(shù)據(jù)。
關(guān)聯(lián)輸出
關(guān)聯(lián)屬性的輸出和模型的輸出轉(zhuǎn)換一樣,使用模型的toArray方法可以同時(shí)輸出關(guān)聯(lián)屬性(對(duì)象),例如:
$user = User::get(1,'profile');
$data = $user->toArray();
dump($data);
$data = $user->toJson();
dump($data);
對(duì)于使用了關(guān)聯(lián)預(yù)載入查詢和手動(dòng)獲取了關(guān)聯(lián)屬性(延遲關(guān)聯(lián)查詢)的情況,
toArray和toJson方法都會(huì)包含關(guān)聯(lián)數(shù)據(jù)。
可以調(diào)用visible和hidden方法對(duì)當(dāng)前模型以及關(guān)聯(lián)模型的屬性進(jìn)行輸出控制,下面來(lái)看一個(gè)例子:
$user = User::get(1, 'profile');
$data = $user->hidden(['name', 'profile.email'])->toArray();
上面的代碼返回的data數(shù)據(jù)中不會(huì)包含用戶模型的name屬性以及關(guān)聯(lián)profile模型的email屬性。
如果要隱藏多個(gè)關(guān)聯(lián)屬性的話,可以使用下面的方式:
$user = User::get(1, 'profile');
$data = $user->hidden(['name', 'profile' => ['email', 'address']])->toArray();
模型的visible方法(用于設(shè)置需要輸出的屬性)的用戶和hidden一致,在此不再多說(shuō),有一點(diǎn)必須強(qiáng)調(diào)下,同時(shí)調(diào)用visible和hidden方法的話,visible是優(yōu)先的,所以下面的profile關(guān)聯(lián)屬性輸出會(huì)包含email和sex。
$user = User::get(1, 'profile');
$data = $user->visible(['profile' => ['email', 'sex']])->hidden(['name', 'profile' => ['email', 'address']])->toArray();
在需要的時(shí)候,即使之前沒(méi)有進(jìn)行任何的關(guān)聯(lián)查詢,你也可以在輸出的時(shí)候追加關(guān)聯(lián)屬性,例如:
$user = User::get(1);
$user->append(['profile'])->toArray();
該例子在調(diào)用toArray方法的時(shí)候才會(huì)進(jìn)行profile關(guān)聯(lián)數(shù)據(jù)獲取并轉(zhuǎn)換輸出。
對(duì)于數(shù)據(jù)集查詢,如果返回類型是數(shù)據(jù)集對(duì)象仍然支持調(diào)用
visible、hidden和append方法,如果不是數(shù)據(jù)集對(duì)象的話可以先用collection助手函數(shù)轉(zhuǎn)換為數(shù)據(jù)集對(duì)象。
$users = User::all();
$data = $users->hidden(['name', 'profile' => ['email', 'address']])
->toArray();
關(guān)聯(lián)實(shí)例
在學(xué)習(xí)完了關(guān)聯(lián)查詢、自定義條件查詢、關(guān)聯(lián)(及嵌套)預(yù)載入、延遲預(yù)載入、關(guān)聯(lián)約束和關(guān)聯(lián)統(tǒng)計(jì)后,我們已經(jīng)基本上掌握了關(guān)聯(lián)的所有查詢操作,現(xiàn)在我們來(lái)通過(guò)一些實(shí)例來(lái)復(fù)習(xí)下關(guān)聯(lián)查詢操作, 以及了解下不同的關(guān)聯(lián)類型的新增、更新和刪除等操作,及其注意事項(xiàng)。
其實(shí)只要理解模型和對(duì)象的概念,關(guān)聯(lián)的新增、更新和刪除,甚至其它的業(yè)務(wù)邏輯操作的調(diào)用都是很容易掌握的。
本節(jié)涉及的關(guān)聯(lián)實(shí)例,各個(gè)模型對(duì)應(yīng)的數(shù)據(jù)表結(jié)構(gòu)如下(本示例僅僅演示關(guān)聯(lián)的用法,不打算重復(fù)強(qiáng)調(diào)模型本身的功能,因此對(duì)數(shù)據(jù)表結(jié)構(gòu)做了必要的簡(jiǎn)化以達(dá)到說(shuō)明的效果):
city
id - integer
name - string
user
id - integer
name - integer
email - string
city_id - integer
role
id - integer
name - string
auth
user_id - integer
role_id - integer
add_time - dateTime
blog
id - integer
name - string
title - string
cate_id - integer
user_id - integer
content
id - integer
blog_id - integer
data - text
cate
id - integer
name - string
title - string
comment
id - integer
content - text
commentable_id - integer
commentable_type - string
模型類分別如下:
City模型
<?php
namespace app\index\model;
use think\Model;
class City extends Model
{
/**
* 獲取城市的用戶
*/
public function users()
{
return $this->hasMany('User');
}
/**
* 獲取城市的所有博客
*/
public function blog()
{
return $this->hasManyThrough('Blog', 'User');
}
}
User模型
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
/**
* 獲取用戶所屬的角色信息
*/
public function roles()
{
return $this->belongsToMany('Role', 'auth');
}
/**
* 獲取用戶發(fā)表的博客信息
*/
public function blogs()
{
return $this->hasMany('Blog');
}
/**
* 獲取所有針對(duì)用戶的評(píng)論
*/
public function comments()
{
return $this->morphMany('Comment', 'commentable');
}
}
Role模型
<?php
namespace app\index\model;
use think\Model;
class Role extends Model
{
/**
* 獲取角色下面的用戶信息
*/
public function users()
{
return $this->belongsToMany('User', 'auth');
}
}
Blog模型
<?php
namespace app\index\model;
use think\Model;
class Blog extends Model
{
/**
* 獲取博客所屬的用戶
*/
public function user()
{
return $this->belongsTo('User');
}
/**
* 獲取博客的內(nèi)容
*/
public function content()
{
return $this->hasOne('Content');
}
/**
* 獲取所有博客所屬的分類
*/
public function cate()
{
return $this->belongsTo('Cate');
}
/**
* 獲取所有針對(duì)文章的評(píng)論
*/
public function comments()
{
return $this->morphMany('Comment', 'commentable');
}
}
Content模型
<?php
namespace app\index\model;
use think\Model;
class Content extends Model
{
/**
* 獲取內(nèi)容所屬的博客信息
*/
public function blog()
{
return $this->belongsTo('Blog');
}
}
Cate模型
<?php
namespace app\index\model;
use think\Model;
class Cate extends Model
{
/**
* 獲取分類下的所有博客信息
*/
public function blogs()
{
return $this->hasMany('Blog');
}
}
Comment模型
<?php
namespace app\index\model;
use think\Model;
class Comment extends Model
{
/**
* 獲取評(píng)論對(duì)應(yīng)的多態(tài)模型
*/
public function commentable()
{
return $this->morphTo();
}
}
關(guān)于不同關(guān)聯(lián)方法的參數(shù)說(shuō)明請(qǐng)參考關(guān)聯(lián)定義部分,這里不再重復(fù)敘述。
auth數(shù)據(jù)表不需要?jiǎng)?chuàng)建模型,對(duì)于多對(duì)多關(guān)聯(lián)來(lái)說(shuō),中間表是不需要關(guān)注的。
一對(duì)一關(guān)聯(lián)
一對(duì)一關(guān)聯(lián)包含hasOne和belongsTo兩種關(guān)聯(lián)關(guān)系定義,系統(tǒng)對(duì)一對(duì)一關(guān)聯(lián)尤其是hasOne做了強(qiáng)化支持,這里用博客模型和內(nèi)容模型之間的關(guān)聯(lián)為例說(shuō)明。
先來(lái)說(shuō)下普通情況的關(guān)聯(lián)操作。
[ 新增 ]
$blog = new Blog;
$blog->name = 'thinkphp';
$blog->title = 'ThinkPHP5關(guān)聯(lián)實(shí)例';
if ($blog->save()) {
$content = new Content;
$content->data = '實(shí)例內(nèi)容';
$blog->content()->save($content);
}
當(dāng)然,支持使用數(shù)組方式新增數(shù)據(jù),例如:
$data = [
'name' => 'thinkphp',
'title' => 'ThinkPHP5關(guān)聯(lián)實(shí)例',
];
$blog = Blog::create($data);
$content = [
'data' => '實(shí)例內(nèi)容',
];
$blog->content()->save($content);
[ 查詢 ]
普通關(guān)聯(lián)查詢
$blog = Blog::get(1);
echo $blog->content->data;
預(yù)載入關(guān)聯(lián)查詢
$blog = Blog::get(1,'content');
echo $blog->content->data;
數(shù)據(jù)集查詢
$blogs = Blog::with('content')->select();
foreach ($blogs as $blog) {
dump($blog->content->data);
}
默認(rèn)一對(duì)一關(guān)聯(lián)查詢也是使用2次查詢,如果希望獲取更好的性能,可以修改關(guān)聯(lián)定義為:
/**
* 獲取博客的內(nèi)容
*/
public function content()
{
// 修改關(guān)聯(lián)查詢方式為JOIN查詢方式
return $this->hasOne('Content')->setEagerlyType(0);
}
修改后,關(guān)聯(lián)查詢從原來(lái)默認(rèn)的IN查詢改為JOIN查詢,可以減少一次查詢,但有一個(gè)地方必須注意,指定的關(guān)聯(lián)表字段
field方法必須改為withField方法。
[ 更新 ]
// 查詢
$blog = Blog::get(1);
// 更新當(dāng)前模型
$blog->title = '更改標(biāo)題';
$blog->save();
// 更新關(guān)聯(lián)模型
$blog->content->data = '更新內(nèi)容';
$blog->content->save();
[ 刪除 ]
// 查詢
$blog = Blog::get(1);
// 刪除當(dāng)前模型
$blog->delete();
// 刪除關(guān)聯(lián)模型
$blog->content->delete();
為了更簡(jiǎn)單的使用一對(duì)一關(guān)聯(lián)的寫入操作,系統(tǒng)提供了關(guān)聯(lián)自動(dòng)寫入功能(V5.0.5+版本開始支持),比較下面的代碼就會(huì)發(fā)現(xiàn)寫入操作和之前的寫法更簡(jiǎn)潔了。
[ 新增 ]
$blog = new Blog;
$blog->name = 'thinkphp';
$blog->title = 'ThinkPHP5關(guān)聯(lián)實(shí)例';
$blog->content = ['data' => '實(shí)例內(nèi)容'];
$blog->together('content')->save();
當(dāng)然,還可以更加對(duì)象化一些,例如:
$blog = new Blog;
$blog->name = 'thinkphp';
$blog->title = 'ThinkPHP5關(guān)聯(lián)實(shí)例';
$content = new Content;
$content->data = '實(shí)例內(nèi)容';
$blog->content = $content;
$blog->together('content')->save();
甚至可以把關(guān)聯(lián)屬性合并到主模型進(jìn)行賦值后寫入,只需要改成:
$blog = new Blog;
$blog->name = 'thinkphp';
$blog->title = 'ThinkPHP5關(guān)聯(lián)實(shí)例';
$blog->data = '實(shí)例內(nèi)容';
$blog->together(['content' => ['data']])->save();
如果不想這么麻煩每次調(diào)用
together方法,也可以直接在模型類中定義relationWrite屬性,但必須是數(shù)組方式。不過(guò)考慮到模型的獨(dú)立操作的可能性,并不建議。
[ 查詢 ]
關(guān)聯(lián)查詢支持把關(guān)聯(lián)模型的屬性直接附加到當(dāng)前模型
$blog = Blog::get(1);
$blog->appendRelationAttr('content', 'data');
echo $blog->data;
如果不想每次都附加操作的話,可以修改Blog模型的關(guān)聯(lián)定義如下:
/**
* 獲取博客的內(nèi)容
*/
public function content()
{
return $this->hasOne('Content')->bind('data');
}
現(xiàn)在就可以直接使用
$blog = Blog::get(1, 'content');
echo $blog->data;
數(shù)據(jù)集的用法基本上類似。
[ 更新 ]
采用關(guān)聯(lián)自動(dòng)更新的寫法如下:
// 查詢
$blog = Blog::get(1);
$blog->title = '更改標(biāo)題';
$blog->content = ['data' => '更新內(nèi)容'];
// 更新當(dāng)前模型及關(guān)聯(lián)模型
$blog->together('content')->save();
更加對(duì)象化的寫法是:
// 查詢
$blog = Blog::get(1);
$blog->title = '更改標(biāo)題';
$blog->content->data = '更新內(nèi)容';
// 更新當(dāng)前模型及關(guān)聯(lián)模型
$blog->together('content')->save();
一樣可以支持關(guān)聯(lián)屬性合并到主模型操作
// 查詢
$blog = Blog::get(1);
$blog->title = '更改標(biāo)題';
$blog->data = '更新內(nèi)容';
// 更新當(dāng)前模型及關(guān)聯(lián)模型
$blog->together(['content' => 'data'])->save();
在關(guān)聯(lián)方法中使用
bind方法把關(guān)聯(lián)屬性綁定到當(dāng)前模型并不會(huì)影響關(guān)聯(lián)寫入,必須使用數(shù)組方式來(lái)明確告知當(dāng)前模型哪些屬性是關(guān)聯(lián)的綁定屬性。
[ 刪除 ]
關(guān)聯(lián)自動(dòng)刪除的操作很簡(jiǎn)單
// 查詢
$blog = Blog::get(1);
// 刪除當(dāng)前及關(guān)聯(lián)模型
$blog->together('content')->delete();
一對(duì)多關(guān)聯(lián)
一對(duì)多關(guān)聯(lián)包括hasMany和belongsTo兩種關(guān)聯(lián)關(guān)系,我們以用戶和博客模型為例來(lái)說(shuō)明,其實(shí)一對(duì)多關(guān)聯(lián)主要是查詢?yōu)橹鳎P(guān)聯(lián)寫入比起單獨(dú)模型的操作并沒(méi)有任何優(yōu)勢(shì),所以建議一對(duì)多的關(guān)聯(lián)寫入仍然由各個(gè)獨(dú)立模型完成,請(qǐng)不要糾結(jié)。
可以查詢某個(gè)用戶的博客
$user = User::get(1);
// 獲取用戶的所有博客
dump($user->blogs);
// 也可以進(jìn)行條件搜索
dump($user->blogs()->where('cate_id', 1)->select());
如果需要對(duì)關(guān)聯(lián)數(shù)據(jù)進(jìn)行額外的條件查詢、更新和刪除操作就可以使用blogs方法。
反過(guò)來(lái),如果需要查詢博客所屬的用戶信息,可以使用
$blog = Blog::get(1);
dump($blog->user->name);
遠(yuǎn)程一對(duì)多
遠(yuǎn)程一對(duì)多的作用是跨過(guò)一個(gè)中間模型操作查詢另外一個(gè)遠(yuǎn)程模型的關(guān)聯(lián)數(shù)據(jù),而這個(gè)遠(yuǎn)程模型通常和當(dāng)前模型是沒(méi)有任何關(guān)聯(lián)的,用前面的例子來(lái)說(shuō)的話就是:
- 一個(gè)用戶發(fā)表了多個(gè)博客;
- 一個(gè)城市有多個(gè)用戶;
- 假設(shè)城市和博客之間沒(méi)有直接關(guān)聯(lián);
如果需要獲取某個(gè)城市下面的所有博客,利用已經(jīng)掌握的關(guān)聯(lián)概念是可以實(shí)現(xiàn)的,只是需要通過(guò)兩次關(guān)聯(lián)操作來(lái)獲取,代碼看起來(lái)類似下面:
$city = City::getByName('shanghai');
$blogs = [];
foreach ($city->users as $user) {
$blogs[$user->id] = $user->blogs()->order('id desc')->limit(100)->select();
}
// 然后對(duì)博客數(shù)據(jù)進(jìn)行額外組裝處理
// ...
雖然思路還是比較清晰,但略顯麻煩,另外還要對(duì)數(shù)據(jù)進(jìn)行組裝,而且不便于統(tǒng)一排序和限制,例如希望一共取出100個(gè)博客數(shù)據(jù)就不好辦。
為了簡(jiǎn)化這種操作,我們引入了遠(yuǎn)程一對(duì)多的關(guān)聯(lián)關(guān)系來(lái)更好的解決,在City模型中已經(jīng)定義了blogs關(guān)聯(lián),實(shí)現(xiàn)方案修改如下:
$city = City::getByName('shanghai');
$blogs = $city->blogs()
->order('id desc')
->limit(100)
->select();
看起來(lái)是不是直觀很多,而且對(duì)博客數(shù)據(jù)的自定義查詢也相當(dāng)方便,無(wú)論是性能還是功能都更佳,因?yàn)槲覀儾恍枰獙?duì)用戶模型進(jìn)行查詢操作。當(dāng)然,很多朋友會(huì)說(shuō),直接在博客模型中添加城市id豈不是更簡(jiǎn)單,這是架構(gòu)設(shè)計(jì)的問(wèn)題了,不屬于本次討論的范疇,本實(shí)例的假設(shè)前提是城市和博客模型之間沒(méi)有任何直接關(guān)聯(lián)。
但有一個(gè)結(jié)論是顯而易見的:架構(gòu)的優(yōu)化對(duì)于代碼的優(yōu)化來(lái)說(shuō)有時(shí)候更有效。
多對(duì)多關(guān)聯(lián)
多對(duì)多關(guān)聯(lián)較前面兩種關(guān)聯(lián)來(lái)說(shuō)復(fù)雜很多,但越是復(fù)雜越能體現(xiàn)出模型關(guān)聯(lián)的優(yōu)勢(shì),下面我們以用戶和角色模型來(lái)看下如何操作多對(duì)多關(guān)聯(lián)。
多對(duì)多關(guān)聯(lián)關(guān)系必然會(huì)有一個(gè)中間表,最少必須包含兩個(gè)字段,例如auth表就包含了user_id 和 role_id(建議對(duì)這兩個(gè)字段設(shè)置聯(lián)合唯一索引),但中間表仍然可以包含額外的數(shù)據(jù)。
中間表不需要?jiǎng)?chuàng)建任何模型(auth表沒(méi)有對(duì)應(yīng)模型),多對(duì)多關(guān)聯(lián)關(guān)系會(huì)創(chuàng)建一個(gè)虛擬的中間表模型(也稱之為樞紐模型)Pivot,對(duì)中間表的所有操作只需要對(duì)該模型進(jìn)行操作即可,事實(shí)上,一般情況下你根本無(wú)需關(guān)注中間表的存在就可以輕松完成多對(duì)多關(guān)聯(lián)操作。
多對(duì)多的關(guān)聯(lián)寫入操作一般有下列幾種方式:
- 用戶和角色數(shù)據(jù)獨(dú)立寫入,然后通過(guò)關(guān)聯(lián)完成中間表的寫入;
- 用戶數(shù)據(jù)獨(dú)立寫入,然后通過(guò)關(guān)聯(lián)完成角色數(shù)據(jù)和中間表數(shù)據(jù)寫入;
- 角色數(shù)據(jù)獨(dú)立寫入,然后通過(guò)關(guān)聯(lián)完成用戶數(shù)據(jù)和中間表數(shù)據(jù)寫入(多對(duì)多關(guān)聯(lián)相互之間操作是等同的,因此本質(zhì)上和上面是同一種方式);
- 通過(guò)關(guān)聯(lián)單獨(dú)完成中間表數(shù)據(jù)更新及刪除;
多對(duì)多的關(guān)聯(lián)寫入操作主要需要掌握下面兩個(gè)方法,我們后面會(huì)詳細(xì)講解,除非模型獨(dú)立操作,一般不需要使用save方法。
| 方法 | 描述 |
|---|---|
attach |
附加關(guān)聯(lián)的一個(gè)中間表數(shù)據(jù) |
detach |
解除關(guān)聯(lián)的一個(gè)或者多個(gè)中間表數(shù)據(jù) |
首先完成第一種方式,僅僅操作中間表數(shù)據(jù)。
// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('admin');
// 增加用戶-角色數(shù)據(jù)
$user->roles()->attach($role->id);
如果中間表有額外數(shù)據(jù)需要寫入,可以使用:
// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('admin');
// 傳入中間表的額外屬性
$user->roles()->attach($role->id, ['add_time' => '2017-1-18']);
事實(shí)上,attach方法是一個(gè)很智能的方法,第一個(gè)參數(shù)能夠識(shí)別包括數(shù)字、字符串、數(shù)組和模型實(shí)例并做出不同的處理。
| 參數(shù)類型 | 作用描述 |
|---|---|
| 數(shù)字或字符串 | 要附加中間表的關(guān)聯(lián)模型主鍵 |
| 索引數(shù)組 | 首先寫入關(guān)聯(lián)模型,然后附加中間表 |
| 普通數(shù)組 | 附加多個(gè)關(guān)聯(lián)數(shù)據(jù)的主鍵 |
| 模型實(shí)例 | 附加關(guān)聯(lián)模型 |
如果要添加的角色尚未創(chuàng)建,則可以使用下面的方式添加用戶-角色數(shù)據(jù):
// 查詢用戶
$user = User::get(1);
// 增加用戶-角色數(shù)據(jù) 并同時(shí)創(chuàng)建新的角色
$user->roles()->attach([
// 添加一個(gè)編輯角色
'name' => 'editor',
]);
如果需要獲取新增的角色表自增主鍵ID,最新版本的attach方法返回的是一個(gè)Pivot模型對(duì)象。
// 查詢用戶
$user = User::get(1);
// 增加用戶-角色數(shù)據(jù) 并同時(shí)創(chuàng)建新的角色
$pivot = $user->roles()->attach([
// 添加一個(gè)編輯角色
'name' => 'editor',
], ['add_time' => '2017-1-31']);
// 獲取中間表的數(shù)據(jù)
echo $pivot->role_id;
echo $pivot->user_id;
echo $pivot->add_time;
下面則表示給用戶添加多個(gè)角色授權(quán):
// 查詢用戶
$user = User::get(1);
// 給用戶授權(quán)多個(gè)角色(根據(jù)角色主鍵)
$user->roles()->attach([1, 2, 3], ['add_time' => '2017-1-31']);
要解除一個(gè)用戶的角色,可以使用:
// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('admin');
// 刪除中間表數(shù)據(jù)
$user->roles()->detach($role->id);
可以同時(shí)解除用戶的多個(gè)角色權(quán)限
// 查詢用戶
$user = User::get(1);
// 刪除中間表數(shù)據(jù)
$user->roles()->detach([1, 2, 3]);
解除用戶的所有角色可以用
// 查詢用戶
$user = User::get(1);
// 刪除中間表數(shù)據(jù)
$user->roles()->detach();
如果需要解除用戶的權(quán)限同時(shí)刪除這個(gè)角色,可以使用:
// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('test');
// 刪除中間表數(shù)據(jù)以及關(guān)聯(lián)表數(shù)據(jù)
$user->roles()->detach($role->id,true);
多對(duì)多關(guān)聯(lián)的查詢和其它關(guān)聯(lián)類似(一樣支持關(guān)聯(lián)自定義查詢),區(qū)別在于每個(gè)關(guān)聯(lián)模型數(shù)據(jù)還有一個(gè)額外的樞紐模型數(shù)據(jù),例如:
// 查詢用戶
$user = User::get(1);
// 獲取用戶的角色
$roles = $user->roles;
foreach ($roles as $role) {
// 輸出用戶的角色名
echo $role->name;
// 獲取中間表模型
dump($role->pivot);
}
多態(tài)一對(duì)多
多態(tài)關(guān)聯(lián)允許一個(gè)模型在單個(gè)關(guān)聯(lián)定義方法中從屬一個(gè)以上其它模型,例如用戶可以評(píng)論書和文章,但評(píng)論表通常都是同一個(gè)數(shù)據(jù)表的設(shè)計(jì)。多態(tài)一對(duì)多關(guān)聯(lián)關(guān)系,就是為了滿足類似的使用場(chǎng)景而設(shè)計(jì)。
多態(tài)一對(duì)多關(guān)聯(lián)主要涉及的是關(guān)聯(lián)查詢,關(guān)聯(lián)寫入本身不建議通過(guò)關(guān)聯(lián)操作完成,請(qǐng)確保用各自的模型獨(dú)立完成數(shù)據(jù)寫入。
多態(tài)一對(duì)多的多態(tài)表設(shè)計(jì)很重要,例如本例子中的評(píng)論表因?yàn)樾枰4娑鄠€(gè)模型的評(píng)論數(shù)據(jù),就可以設(shè)計(jì)成多態(tài)關(guān)聯(lián)。
要獲取博客的評(píng)論數(shù)據(jù)可以使用:
$blog = Blog::get(1);
foreach ($blog->comments as $comment) {
dump($comment);
}
當(dāng)然,一樣可以進(jìn)行評(píng)論篩選過(guò)濾
$blog = Blog::get(1);
$comments = $blog->comments()
->where('content', 'like', '%think%')
->order('id desc')
->limit(20)
->select();
foreach ($comments as $comment) {
echo $comment->content;
}
對(duì)于評(píng)論模型來(lái)說(shuō),則可以這樣操作
$comment = Comment::get(1);
$commentable = $comment->commentable;
Comment 模型的 commentable 關(guān)聯(lián)會(huì)返回 Blog 或 User 模型的對(duì)象實(shí)例,這取決于評(píng)論所屬模型的類型。
如果你的多態(tài)類型字段保存的數(shù)據(jù)并非是模型名稱之類的,而是采用數(shù)字保存(提高存儲(chǔ)和查詢性能),比如1表示博客,2表示用戶。
關(guān)聯(lián)定義方法需要對(duì)應(yīng)修改為:
Blog模型
/**
* 獲取所有針對(duì)文章的評(píng)論
*/
public function comments()
{
return $this->morphMany('Comment', 'commentable', 1);
}
User模型
/**
* 獲取所有針對(duì)用戶的評(píng)論
*/
public function comments()
{
return $this->morphMany('Comment', 'commentable', 2);
}
Comment模型
/**
* 獲取評(píng)論對(duì)應(yīng)的多態(tài)模型
*/
public function commentable()
{
return $this->morphTo(null, [
'1' => 'Blog',
'2' => 'User',
]);
}
如果你的模型使用不同的命名空間,可以使用完整的命名空間方式定義:
/**
* 獲取評(píng)論對(duì)應(yīng)的多態(tài)模型
*/
public function commentable()
{
return $this->morphTo(null, [
'1' => 'app\model\Blog',
'2' => 'app\model\User',
]);
}
總結(jié)
本章我們了解了模型關(guān)聯(lián)的概念,并著重學(xué)習(xí)了關(guān)聯(lián)的查詢,并針對(duì)不同的關(guān)聯(lián)類型給出了實(shí)際的關(guān)聯(lián)操作指引,下一章我們會(huì)來(lái)說(shuō)下數(shù)據(jù)庫(kù)和模型操作的性能和安全方面的話題。
上一篇:第七章:模型高級(jí)用法
下一篇:第九章:性能和安全