### 2018 年 11 月 24 日 發布
## 前言
ThinkPHP5的模型關聯是按照面向對象的開發思想重構的,和`3.2`時代的關聯模型實現和用法完全不同。因此,要掌握`5.*`的模型關聯,必須要有面向對象的思想意識,重新認識新版的模型關聯,這是理解和掌握模型關聯的關鍵。
掌握模型關聯的關鍵是掌握關聯的定義和查詢,關聯的寫入完全可以用模型自身的寫入來完成,而不要依賴關聯寫入(只有某些特殊的關聯才需要單獨的關聯寫入)。
本文主要為大家盡量淺顯的講解下模型關聯的查詢操作和注意事項,主要指導開發者打通模型關聯的使用障礙和恐懼,從而可以漸入佳境的學習模型關聯。由于模型關聯涉及的面比較廣,沒法做到面面俱到,因此本文內容不做太多深入,以免形成長篇累牘,更加詳細的模型關聯用法建議參考官方的《[掌握ThinkPHP5.0數據庫和模型](http://www.hmoore.net/thinkphp/master-database-and-model)》教程中的「[第八章:模型關聯](http://www.hmoore.net/thinkphp/master-database-and-model/265559)」。
## 基本用法
關聯定義是第一步,如果沒法正確的完成關聯定義,你將不得其門而入。但其實關聯的定義方法本身很簡單,難點在于判斷你的業務操作應該使用什么關聯關系,而具體表之間的關系其實在做數據庫架構設計的時候已經明確了的。
關聯關系通常有一個參照模型,這個參照模型我們一般稱為主模型(或者當前模型),關聯關系對應的模型就是關聯模型,關聯關系是指定義在主模型中的關聯關系,有些關聯關系還會設計到一個中間表的概念,但中間表不一定需要存在具體的模型。關聯模型本身也是一個普通的獨立模型,具備模型的所有功能。兩個模型之間的關聯關系就是通過關聯方法來定義的。
目前已經支持的關聯關系如下:
|模型方法|關聯類型|
|---|---|
|`hasOne`|一對一HAS ONE|
|`belongsTo`|一對一BELONGS TO|
|`hasMany`|一對多 HAS MANY|
|`hasManyThrough`|遠程一對多 HAS MANY THROUTH|
|`belongsToMany`|多對多 BELONGS TO MANY|
|`morphMany`|多態一對多 MORPH MANY|
|`morphTo`|多態 MORPH TO|
>[info] 具體的關聯定義方法可以參考官方手冊,不是文本的重點,因此不做詳細說明。
以一個簡單的博客和分類表為例,每個分類下有多個博客,這就屬于一對多關聯(`hasMany`),每篇博客都屬于某個分類,這就是一對一關聯(`blongsTo`),博客模型的`cate_id`和分類模型的`id`屬性則是兩個模型關聯的紐帶和約束。
具體的關聯定義如下:
博客模型
```
<?php
namespace app\index\model;
use think\Model;
class Blog extends Model
{
/**
* 獲取博客所屬的分類
*/
public function cate()
{
return $this->belongsTo('Cate');
}
}
```
如果你的模型都在同一個命名空間下,直接使用模型名稱就行,如果不在同一個命名空間下,則需要給出完整的類名,例如:
```
<?php
namespace app\index\model;
use think\Model;
class Blog extends Model
{
/**
* 獲取博客所屬的分類
*/
public function cate()
{
return $this->belongsTo(\app\common\Cate::class);
}
}
```
分類模型
```
<?php
namespace app\index\model;
use think\Model;
class Cate extends Model
{
/**
* 獲取分類下的所有博客信息
*/
public function blogs()
{
return $this->hasMany('Blog');
}
}
```
當我們查詢到某個博客數據的時候,可以很簡單的通過模型關聯獲取到分類數據。
```
// 查詢博客數據
$blog = Blog::find(3);
// 獲取博客所屬分類模型
$cate = $blog->cate;
```
如果沒有使用模型關聯的設計,你必須使用下面的方法獲取分類數據。
```
// 查詢博客數據
$blog = Blog::find(3);
// 獲取博客所屬分類模型
$cate = Cate::where('id', $blog->cate_id)->find();
```
很明顯,模型關聯能夠簡化多個模型之間的查詢,關聯關系越是復雜,帶來的效果更明顯。
需要的時候關聯關系本身還可以多級嵌套,例如:
```
// 查詢博客數據
$blog = Blog::find(3);
// 獲取作者的檔案
$info = $blog->user->profile;
```
通過模型的屬性方式是獲取關聯模型數據的最簡單的一種方式,該方式會查詢滿足關聯條件的所有關聯數據。
對于一對多關聯而言,如果只需要獲取部分關聯數據,就需要改成關聯方法的調用方式,下面一個例子給出了兩種方式代碼的區別。
```
$cate = Cate::find(1);
// 獲取當前分類下所有的博客
$blogs = $cate->blogs;
// 獲取當前分類的最新三條博客
$blogs = $cate->blogs()
->order('create_time', 'desc')
->limit(3)
->select();
```
至于具體的關聯方法怎么定義,可以參考官方手冊,已經給出了明確的說明,就不再多說了,這里總結一些注意事項。
## 駝峰命名的關聯方法獲取
關于關聯定義有一個很多開發者容易混淆的地方,所有的關聯方法定義必須使用首字母小寫的駝峰命名規范,但在獲取關聯屬性的時候,則推薦使用小寫+下劃線方式的對應屬性名稱,但方法調用依然是保持駝峰命名。
舉例說明如下:
```
<?php
namespace app\index\model;
use think\Model;
class Cate extends Model
{
/**
* 獲取分類下的所有博客信息
*/
public function currentBlogs()
{
return $this->hasMany('Blog');
}
}
```
關聯查詢代碼
```
$cate = Cate::find(1);
// 獲取當前分類下所有的博客
$blogs = $cate->current_blogs;
// 獲取當前分類的最新三條博客
$blogs = $cate->currentBlogs()
->order('create_time', 'desc')
->limit(3)
->select();
```
## 一個關聯關系可以定義多個關聯方法
一個關聯關系并不代表只能定義一個關聯方法,我們可以為不同的查詢需求定義多個不同的關聯方法,以分類和博客的一對多關聯關系為例,我們可以這樣定義。
```
<?php
namespace app\index\model;
use think\Model;
class Cate extends Model
{
/**
* 獲取分類下的所有博客信息
*/
public function blogs()
{
return $this->hasMany('Blog');
}
/**
* 獲取分類下的最新三條博客信息
*/
public function lastThreeBlog()
{
return $this->hasMany('Blog')
->order('create_time', 'desc')
->limit(3);
}
}
```
>[info] 你可以在模型關聯方法中使用查詢構造器完成對關聯數據的條件約束,`5.1`版本的話還可以支持在關聯方法中調用模型的自定義方法。
可以通過下面的代碼來調用不同的關聯數據。
```
$cate = Cate::find(1);
// 獲取當前分類下所有的博客
$blogs = $cate->blogs;
// 獲取當前分類的最新三條博客
$blogs = $cate->last_three_blog
```
## 關聯方法定義支持參數
還是上面的需求,我們希望獲取最近的N條博客數據,但希望具體多少條在查詢的時候傳入,這需要我們首先給關聯方法增加一個參數。
```
/**
* 獲取分類下的最新N條博客信息
*/
public function lastBlog($number = 3)
{
return $this->hasMany('Blog')
->order('create_time', 'desc')
->limit($number);
}
```
查詢的時候使用
```
$cate = Cate::find(1);
// 獲取當前分類下最新的10條博客
$blogs = $cate->lastBlog(10)->select();
```
## 典型的`N+1`查詢問題
如果要查詢3個分類,以及每個分類的博客數據,按照普通的關聯查詢就會產生3+1次查詢,隨著數據量的越來越大查詢次數會越來越多,性能也會急劇下降。
```
$cates = Cate::select([1,2,3]);
foreach($cates as $cate) {
$blogs = $cate->blogs;
}
```
如果查看頁面Trace信息可以看到當前產生了4次查詢操作。
預載入查詢就是為了解決這個`N+1`問題而應運而生的,我們只要把代碼改成如下:
```
$cates = Cate::with(['blogs'])->select([1,2,3]);
foreach($cates as $cate) {
$blogs = $cate->blogs;
}
```
你會發現最終產生的查詢次數為2次,事實上無論有多少分類數據,最終的查詢次數都是2次,很好的解決了查詢性能問題。
>[info] `with`方法支持同時指定多個關聯方法,因此使用數組參數是更好的規范。
## 預載入查詢的數據篩選
預載入查詢的數據篩選有兩種方式,第一種方式前面我們已經介紹過了,就是為該關聯關系增加額外的方法定義,然后在方法里面進行查詢條件的篩選,這種對于有明確的篩選需求比較有效,查詢代碼也比較簡單。
```
$cates = Cate::with(['last_three_blog'])
->select([1,2,3]);
```
>[info] `with`方法中的關聯方法名可以是實際的關聯定義方法名,也可以是關聯方法名的小寫+下劃線轉換名。
第二種方式就是在預載入查詢的時候通過閉包動態指定查詢條件。
```
// 給預載入查詢指定篩選條件
$cates = Cate::with(['blogs' => function($query){
$query->order('create_time', 'desc')->limit(3);
}])->select([1,2,3]);
```
## 延遲預載入
關聯預載入查詢并非是惰性的,無論最后數據是否需要使用,查詢已經產生了,有些情況下,需要根據查詢出來的數據來決定是否需要使用關聯預載入,使用延遲預載入可以實現關聯數據的惰性查詢,有效提高性能,避免浪費不必要的查詢。
```
$cates = Cate::select([1,2,3]);
// 使用延遲預載入查詢關聯數據
$cates->load('blogs');
foreach($cates as $cate) {
$blogs = $cate->blogs;
}
```
## 關聯統計
經常會有一些對關聯數據進行聚合統計的需求,框架提供了便捷的關聯統計方法。
```
$cates = Cate::withCount(['blogs'])
->select([1,2,3]);
foreach($cates as $cate) {
// 獲取分類下的博客總數
echo $cate->blogs_count;
}
```
如果要改變默認的統計字段名稱,可以改成
```
$cates = Cate::withCount(['blogs' => 'blog_count'])
->select([1,2,3]);
foreach($cates as $cate) {
// 獲取分類下的博客總數
echo $cate->blog_count;
}
```
也支持使用閉包方式進行統計查詢的條件限制
```
$cates = Cate::withCount(['blogs' => function($query) {
$query->where('status', 1);
}])->select([1,2,3]);
foreach($cates as $cate) {
// 獲取分類下的博客總數
echo $cate->blogs_count;
}
```
除了`count`統計外,還支持`sum/max/min/avg`等聚合統計。
```
$cates = Cate::withSum(['blogs' => 'total_read'],'read_count')
->select([1,2,3]);
foreach($cates as $cate) {
// 獲取分類下的博客閱讀總數
echo $cate->total_read;
}
```
>[info] 關聯統計查詢用的是子查詢方式,所以并不會增加額外的查詢次數。
## 關聯數據輸出
使用了關聯查詢后,依然可以使用`hidden`/`visible`/`append`方法進行模型數據的輸出調整。
```
$blog = Blog::with('cate')->find(1);
// 隱藏分類的部分屬性
$blog->hidden(['cate' => ['remark', 'create_time', 'update_time']])
->toArray();
```
## 根據關聯條件查詢
```
// 查詢博客超過10個的分類
$cates = Cate::has('blogs','>',10)->select();
// 查詢最近3天發過博客的分類
$cates = Cate::hasWhere('blogs', function($query) {
$query->whereTime('create_time', '-3 days');
})->select();
```
閉包里面的查詢條件是關聯模型的約束,如果你需要添加主模型的額外約束條件,可以單獨追加使用`where`方法或者其它的查詢構造器,不過要注意由于`hasWhere`方法使用的是`JOIN`查詢,在查詢條件中要指定別名。
```
// 查詢最近3天發過博客并且狀態正常的分類 按name排序
$cates = Cate::hasWhere('blogs', function($query) {
$query->whereTime('create_time', '-3 days');
})->where('Cate.status', 1)
->order('Cate.name')
->select();
```
## 自關聯
如果你的模型關聯到自身(例如子分類和分類的關聯關系),就需要在定義關聯的時候設置自關聯。
```
<?php
namespace app\index\model;
use think\Model;
class Cate extends Model
{
/**
* 獲取分類下的所有博客信息
*/
public function blogs()
{
return $this->hasMany('Blog');
}
/**
* 獲取當前分類的子分類
*/
public function sub()
{
return $this->hasMany('Cate', 'parent_id')
->selfRelation();
}
}
```
關聯查詢代碼
```
$cate = Cate::with('sub')->find(1);
```
- 值得升級到5.1的18個理由
- 5.1.7版本新特性
- JSON字段類型在ORM中的使用
- 文件下載響應對象
- 教你使用5.1的數組對象查詢
- 模型三大利器之一:搜索器
- 在ThinkPHP中使用Yaconf
- 掌握命令行的表格輸出
- 5.1.25查詢參數綁定的改進
- ThinkPHP安全規范指引
- 巧用數據集的排序功能實現統計排序
- think-orm ——基于5.1的獨立ORM庫
- think-template——基于ThinkPHP的獨立模板引擎
- ThinkPHP5.1.26版本發布——修正版本,包含安全更新
- ThinkPHP5.0和3.2再發安全更新
- 官宣:ThinkPHP發布首個LTS版本
- 你真的了解Db類和模型的正確使用姿勢么?
- 如何更有效的記錄和管理日志
- 模型三大利器之二:修改器
- ThinkPHP5.1.28版本發布——修正上一版本問題,改進關聯查詢
- 模型三大利器之三:獲取器
- API版本控制的幾種思路
- ThinkPHP5.2第一個Beta版本發布測試
- 讓你少犯錯的數據查詢基本原則
- ThinkPHP發布5.1.29版本——常規更新
- 這15個好習慣讓你更容易升級到5.2
- 如何有效提高ThinkPHP的應用性能
- 讓你提高開發效率的查詢技巧
- 模型關聯查詢不完全指南
- 5.2發布Beta2版本——統一和精簡大量用法
- ThinkPHP發布5.1.30版本——支持微秒時間字段寫入
- ThinkPHP的數據緩存使用
- ThinkPHP5.2安裝及入口文件
- ThinkPHP榮獲2018 年度最受歡迎中國開源開發框架第1名
- 5.1路由使用心得技巧
- ThinkPHP5.*版本發布安全更新
- ThinkPHP項目及代碼規范指北
- 5.2版本的設計規范指導
- ThinkPHP5.1.32版本發布——圣誕快樂
- 利用Trait特性給模型增加樂觀鎖功能
- 5.2數據庫和模型的變化(摘要)
- ThinkPHP模板引擎實現和常見問題
- ThinkPHP5.0.24版本發布——安全更新
- 不忘初心,方得始終——ThinkPHP十三周年報告
- ThinkPHP5+相關資源匯總
- 異步社區ThinkPHP周年慶專享優惠活動
- 5.2路由的調整和改進
- ThinkPHP發布5.1.33版本——包含安全更新
- ThinkPHP擴展開發指南
- ThinkPHP發布5.2Beta3版本
- ThinkPHP發布5.1.34版本——喜迎新年
- ThinkPHP發布5.2RC1版本
- ThinkPHP發布5.1.35版本——常規更新
- 5.2配置類的調整
- 5.2時間查詢的改進和優化
- 5.2RC版本升級不完全指導(僅供學習參考)
- ThinkPHP5.2版本正式變更為6.0版本
- ThinkPHP百度云云虛擬主機專享免費活動
- 事件系統以及查詢事件、模型事件的使用
- ThinkPHP6.0RC2版本發布——架構升級、精簡核心
- ThinkPHP5.1.36LTS版本發布——常規更新
- 新版Session和Cookie設計變化
- ThinkPHP5.1.37版本發布——常規更新
- ThinkPHP6.0RC3版本發布——細節完善,體驗優化
- 6.0中間件使用詳解
- Composer各大廠商鏡像地址
- ThinkPHP6.0發布計劃公告
- 「ThinkPHP開發者周刊」招募志愿者
- ThinkPHP6.0日志變化
- ThinkPHP5.1.38版本發布——常規更新
- ThinkPHP6.0RC4版本發布——ORM獨立,日志多通道支持
- ThinkORM2.0開發指南上線
- ThinkPHP6.0RC5版本發布——多應用模式獨立,中間件機制調整
- ThinkPHP6.0版本發布——程序員節福利
- ThinkPHP5.1.39LTS版本發布——常規更新
- ThinkPHP6.0.1版本發布——圣誕快樂!
- 回顧2019,展望2020!
- ThinkPHPV6.0.2版本發布——2020新春快樂!
- 周年福利系列:Swoole合作優惠
- 億速云成為ThinkPHPV6.0獨家贊助發布商??
- 新冠疫情工具和限免資源專題(保持更新中)
- 周年福利系列:創宇信用認證合作優惠
- 周年福利系列:碼云企業版限時10%優惠
- 周年福利系列:想天短說抵現優惠
- think-swoole直播:從零開始掌握swoole開發
- 周年福利系列:B2C開源電商ShopXO授權8折優惠
- 周年福利系列:LayuiAdmin 永久授權限時優惠
- ThinkPHP資源導航站上線——構建生態 服務未來
- ThinkPHP官方技術支持服務和應用服務市場上線公測
- ThinkPHP市場精選——推廣基本要素
- ThinkPHP市場精選——客服聊天專題
- ThinkPHPV6.0.3版本發布——端午安康
- ThinkPHP開發者扶持計劃
- 6.0.3版本關鍵更新及升級事項
- 「ThinkPHP開發者周刊」改版重啟
- ThinkPHP市場精選——企業建站專題
- ThinkPHP 提供統一API接口服務
- ThinkPHP市場精選——直播電商專題
- ThinkAPI服務SDK發布
- 官方服務市場啟用獨立子域名
- ThinkPHP市場精選——刷臉支付專題
- ThinkAPI推出會員服務計劃
- ThinkPHPV6.0.4版本發布——中秋國慶雙節快樂
- ThinkPHPV5.1.40版本發布——常規更新
- 1024程序員節福利走一波
- ThinkPHP V6.0.5版本發布——兼容Composer2.0
- 知識圖譜應用場景——源論技術沙龍
- ThinkPHP5.*版本改進Composer2.0的兼容
- 官方市場雙十一精選推薦
- 技術人做產品有機會么(文末送課程)
- 本周秒殺——古德云售后獲客營銷系統
- ThinkAPI服務更新——支持接口分組和PHP版本依賴調整
- PHP8新特性盤點
- PHP8新特性系列:構造器屬性提升使用及注意事項
- ThinkPHP2021新年寄語
- ThinkPHP V6.0.6&V5.1.41版本發布——兼容PHP8.0
- PHP如何更優雅地調用API接口
- ThinkPHP V6.0.7發布——修正版本
- ThinkAPI服務更新——IP白名單
- 最新版ThinkORM對于時間字段的調整
- ThinkAPI短信接口正式上線
- ThinkPHP V6.0.8版本發布——多環境變量配置支持
- 頂想云寫作服務開啟第一次公測
- ThinkSSL上線——官方SSL/TLS證書服務
- MDBootstrap國內用戶福利——ThinkPHP官方市場首發
- ThinkPHP V6.0.9版本發布——常規更新
- ThinkORM功能盤點——虛擬模型
- 全面支持主流GIT版本庫——云寫作服務第二次公測
- 云寫作服務私有化部署方案之:版本庫私有化
- 看云雙十一活動
- ThinkPHP V6.0.10LTS發布——兼容PHP8.1
- ThinkPHP V6.0.12發布——命令行兼容8.1
- 頂想云知識管理上線公測——構建企業文檔中心和知識庫
- 頂想云上線——助力生態數字化建設
- 618活動進行中——官方市場迎來一波更新
- 頂想云知識管理正式上線——看云文檔啟動遷移服務
- ThinkPHP V6.0.13發布——常規更新
- 頂想云網站助理服務上線——構建產品支持服務
- ThinkPHP發布6.1.0&6.0.14版本——安全更新
- ThinkPHP新版社區上線試運營
- ThinkAPI上架人臉核身接口——助力網站實名認證
- 辭舊迎新——舊版社區停止注冊及發帖
- ThinkPHP6.1.2版本發布——兼容PHP8.2