thinkphp中我們常通過使用關聯預載入(Eager Loading)來解決關聯查詢中"N+1 查詢問題", 通過減少數據庫查詢次數來提升性能. 其底層實現邏輯可以分為以下幾個關鍵步驟:
1.關聯定義的基礎
以下面的代理為例子:
//$this->model = new \app\admin\model\device\Relation;
$list = $this->model
->where($where)
->with(['device'])
//主模型默認使用模型名 relation 作為前綴
//關聯模型默認使用關聯方法名作為前綴
->where(['relation.group_id' => $id])
->order($sort, $order)
->paginate($limit);
上面的查詢中使用了關聯預載入,依賴於Relation模型中預先定義的關聯關係.
public function device() {
return $this
->belongsTo('app\admin\model\device\Lists',
'device_id', 'id', [], 'LEFT')
//預載入方式 0 JOIN查詢 1 IN查詢
->setEagerlyType(0);
}
這裏關聯定義包含了關聯類型(如: hasMany, hasOne, belongsTo等), 外鍵(關聯模型中指向主模型的字段), 主鍵(主模型的主鍵)
2.主查詢執行(獲取主表數據)
實際執行的時候, 框架會先執行主表的查詢, 獲取複合條件的主數據(如上覆合主表中relation.group_id=$id的分組數據). 對應的SQL如下:
SELECT * FROM `device_admin_relation` WHERE `group_id` = 1;
3.收集關聯查詢的"條件參數"
主查詢獲取數據後, with(['device']) 會根據關聯定義, 從主數據中提取關聯查詢所需的關鍵字段值(通常是主模型的主鍵).
例如, 主查詢得到的分組數據主鍵id為[1,2,3], 則會收集這些id, 用於後續關聯查詢.
4.批量執行關聯查詢(核心優化點)
with方法的核心作用在這裏體現: 它會根據手機到的主表主鍵, 一次性批量查詢所有關聯數據, 而不是逐行查詢. 以上面的device關聯為例, 框架會生成類似這樣的SQL:
SELECT * FROM `device_lists` WHERE `id` IN (1, 2, 3); -- 用 IN 條件批量查詢
這一步解決了"N+1 問題": 如果不使用with, 循環主數據獲取每個分組的設備時, 會執行N次關聯查詢(N為主數據條數), 加上1次主查詢, 共N+1次; 而使用with後只需要1次主查詢+1次關聯查詢, 總共2次.
5.關聯數據與主數據的"組裝匹配"
關聯查詢得到數據後, 框架會根據關聯定義中的外鍵與主鍵對應關係, 將關聯數據"掛載"到主數據的對應字段上.
最終, 得到的$list數據結構類似如下格式:
[
// 第一條主數據(group.id=1)
[
'id' => 1,
'name' => '分組1',
// 關聯的設備數據(自動掛載到 'device' 字段)
'device' => [
['id' => 101, 'group_id' => 1, 'name' => '設備101'],
['id' => 102, 'group_id' => 1, 'name' => '設備102']
]
],
// 第二條主數據(group.id=2)
[
'id' => 2,
'name' => '分組2',
'device' => [
['id' => 201, 'group_id' => 2, 'name' => '設備201']
]
]
// ...
]
6.支持關聯查詢的"二次篩選"
with還支持通過閉包對關聯查詢進行條件限制,例如:
$list = $this->model
->with(['device' => function($query) {
// 只查詢狀態為正常的設備
$query->where('status', 1);
}])
->order(...)
->select();
此時關聯查詢的SQL會自動帶上額外條件:
SELECT * FROM `device_list` WHERE `group_id` IN (1,2,3) AND `status` = 1;
延伸示例:
//Relation模型中定義的關聯預載入admin方法
public function admin() {
return $this
->belongsTo('app\admin\model\device\Admin',
'device_admin_id',
'id', [], 'LEFT') //設置通過left左連接的方式進行關聯
->setEagerlyType(0); //預載入方式 0 JOIN查詢 1 IN查詢
}
// $this->model = new \app\admin\model\device\Relation;
$list = $this->model
->field("device_admin_id,group_id")
->with(['groups', 'admin'])
->where($where)
->where('admin.deletetime is null') //這裏直接過濾admin關聯中的數據
//篩選當前Relation模型對應數據表字段的時候為了不與其他表字段混淆需要
//加上當前模型前綴relation
->where(['relation.group_id' => $id])
->group('relation.device_admin_id')
->paginate($limit);
最終生成的sql語句如下:
SELECT
`relation`.`device_admin_id`,
`relation`.`group_id`,
groups.id AS groups__id,
groups.group_name AS groups__group_name,
groups.remark AS groups__remark,
groups.createtime AS groups__createtime,
groups.updatetime AS groups__updatetime,
groups.deletetime AS groups__deletetime,
admin.id AS admin__id,
admin.NAME AS admin__name,
admin.idno AS admin__idno,
admin.phone AS admin__phone,
admin.pwd AS admin__pwd,
admin.state AS admin__state,
admin.remark AS admin__remark,
admin.createtime AS admin__createtime,
admin.updatetime AS admin__updatetime,
admin.deletetime AS admin__deletetime
FROM
`fa_device_admin_relation` `relation`
LEFT JOIN `fa_device_group` `groups` ON `relation`.`group_id` = `groups`.`id`
LEFT JOIN `fa_device_admin` `admin` ON `relation`.`device_admin_id` = `admin`.`id`
WHERE
( admin.deletetime IS NULL )
AND `relation`.`group_id` = '6'
GROUP BY
`relation`.`device_admin_id`
總結
with(['device']) 的核心邏輯是:
- 先查主表數據
- 提取主表主鍵, 批量查詢關聯表數據(用IN條件)
- 根據關聯規則(外鍵-主鍵) 將關聯數據匹配到主數據中
通過上面的方式, 大幅減少了數據庫查詢次數, 是關聯查詢中提升星能的關鍵手段.