Thinkphp 第八章:模型關(guān)聯(lián)

模型的關(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),最關(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_idrole_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_typemorph_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)用。

relationwith方法的主要區(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)置到模型類的getall方法的第二個(gè)參數(shù)了,我們后面對(duì)with方法的用法說(shuō)明也均適用于getall方法的第二個(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è)articleshasMany關(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)約束方法,包括hashasWhere兩個(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ù)載入查詢的重要性,模型的getall方法的第二個(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)查詢)的情況,toArraytoJson方法都會(huì)包含關(guān)聯(lián)數(shù)據(jù)。

可以調(diào)用visiblehidden方法對(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)用visiblehidden方法的話,visible是優(yōu)先的,所以下面的profile關(guān)聯(lián)屬性輸出會(huì)包含emailsex。

$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)用visiblehiddenappend方法,如果不是數(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)包含hasOnebelongsTo兩種關(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)包括hasManybelongsTo兩種關(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_idrole_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ì)返回 BlogUser 模型的對(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í)用法
下一篇:第九章:性能和安全

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Eloquent: 關(guān)聯(lián)模型 簡(jiǎn)介 數(shù)據(jù)庫(kù)中的表經(jīng)常性的關(guān)聯(lián)其它的表。比如,一個(gè)博客文章可以有很多的評(píng)論,或者一個(gè)...
    Dearmadman閱讀 17,547評(píng)論 6 16
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,681評(píng)論 1 32
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 12,511評(píng)論 6 13
  • 1.設(shè)計(jì)模式是什么? 你知道哪些設(shè)計(jì)模式,并簡(jiǎn)要敘述?設(shè)計(jì)模式是一種編碼經(jīng)驗(yàn),就是用比較成熟的邏輯去處理某一種類型...
    龍飝閱讀 2,305評(píng)論 0 12
  • 太陽(yáng)落下了時(shí)候可真美呀! 可他卻把江山河水照的更美了,江山就像一件漂亮的衣裳,把河水照的就像一個(gè) 活娃娃水中劃...
    高詩(shī)涵閱讀 224評(píng)論 0 1

友情鏈接更多精彩內(nèi)容