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