ThinkPHP8 模型方法,以及数据库N+1查询问题优化办法

ThinkPHP8 模型方法,以及数据库N+1查询问题优化办法
28.7的博客ThinkPHP中hasOne、hasMany和belongsTo的关联关系详解
本文的一个概念
在ThinkPHP框架中,hasOne、hasMany和belongsTo用于定义模型间的关联关系,分别对应一对一(1:1)、一对多(1:n) 和多对一(n:1) 关系。以下通过示例详细说明:
重要的概念:N+1查询问题
在关联查询中,“N+1 查询问题”是一个典型的性能陷阱,而 ThinkPHP 的 with() 方法通过关联预加载机制从根本上解决了这个问题。下面从概念、影响、形成原因和解决原理四个方面详细说明:
一、什么是 N+1 查询问题?
“N+1 查询问题”指的是:当查询主模型的 N 条记录 时,会先执行 1 次主查询 获取这 N 条记录,然后为每条记录单独执行 1 次关联查询 获取关联数据,最终导致 1 + N 次数据库查询 的低效情况。
例如:查询 10 篇文章,每篇文章需要获取对应的作者信息。若不优化,会产生 1(查文章) + 10(每篇文章查作者) = 11 次查询。
二、N+1 查询的形成方式和条件
形成方式:
- 先查询主模型列表:执行 1 次查询获取主模型的 N 条记录(如“查询所有文章”)。
- 循环中查询关联数据:遍历这 N 条记录,对每条记录单独执行 1 次查询获取关联模型数据(如“循环每篇文章,查询其作者”)。
形成条件:
- 需要查询 主模型的多条记录(N ≥ 1);
- 每条主模型记录都需要 关联查询其他模型的数据;
- 未使用 关联预加载(如
with()方法),而是采用“即时加载”(访问关联属性时才触发查询)。
三、N+1 查询的影响
- 性能急剧下降:数据库查询是 Web 应用中最耗时的操作之一。N 越大(如 N=1000),额外产生的 N 次查询会导致响应时间大幅增加(1001 次查询 vs 2 次查询)。
- 数据库压力增大:频繁的查询请求会占用数据库连接资源,可能导致连接池耗尽、并发能力下降。
- 代码效率低下:循环中嵌套查询的逻辑会让代码执行效率降低,尤其在数据量较大时,问题会被放大。
四、with() 方法如何解决 N+1 查询问题?
with() 方法的核心是 “关联预加载”:在查询主模型数据的同时,一次性批量查询所有关联模型的数据,再通过程序逻辑将关联数据与主模型记录“匹配”,最终只需要 2 次查询(主模型 1 次 + 关联模型 1 次),彻底避免 N 次额外查询。
以本文的代码实例:
假设你的文章模型(Article)通过 author() 方法关联用户模型(TpUserData),现在要查询 10 篇文章及其作者:
无
with()时(N+1 问题):1
2
3
4
5
6
7
8// 1. 第1次查询:获取10篇文章
$articles = Article::limit(10)->select();
foreach ($articles as $article) {
// 2. 每次循环触发1次查询:获取当前文章的作者(共10次)
$author = $article->author;
}
// 总查询次数:1 + 10 = 11 次(N+1)有
with()时(解决 N+1 问题):1
2
3
4
5
6
7
8
9// 1. 第1次查询:获取10篇文章
// 2. 第2次查询:自动提取10篇文章的user_id,用IN条件批量查询所有作者
$articles = Article::with('author')->limit(10)->select();
foreach ($articles as $article) {
// 直接从预加载的缓存中获取作者,无需再查询数据库
$author = $article->author;
}
// 总查询次数:2 次(与N无关)
with() 的执行流程:
- 步骤1:执行主查询,获取主模型的 N 条记录(如 10 篇文章)。
- 步骤2:自动提取这 N 条记录中用于关联的外键(如文章的
user_id),得到一个外键列表(如[1,3,5,...,21])。 - 步骤3:对关联模型执行 1 次批量查询,条件为“关联模型的主键 IN 外键列表”(如“用户表 id IN (1,3,5,…,21)”),一次性获取所有关联数据。
- 步骤4:通过程序逻辑将关联数据与主模型记录匹配(按外键对应),并缓存到主模型对象中。后续访问
$article->author时,直接从缓存中读取,无需再次查询数据库。
总结
- N+1 查询 是因“先查主模型,再循环查关联”导致的低效查询,会显著降低性能。
with()方法 通过“预加载”机制,将 N+1 次查询优化为 2 次查询(主模型 + 批量关联模型),从根本上解决了该问题。- 在实际开发中,只要涉及“查询多条主记录并需要关联数据”,必须使用
with()预加载,这是提升关联查询性能的主要方法。
正文
1. hasOne(一对一关系)
假设存在TpUserData(用户信息)和TpUserAuthentication(用户认证信息)两个模型,一个用户对应一份认证信息,两者通过id关联。
方法定义
1 | hasOne('关联模型类名', '外键', '主键'); |
- 关联模型(必须):关联的模型类名
- 外键:关联模型中用于关联当前模型的字段(默认规则:当前模型名+_id,如
user_id) - 主键:当前模型的主键(默认自动获取,可指定)
模型示例
1 | // TpUserAuthentication模型(关联模型) |
1 | // TpUserData模型(当前模型) |
测试示例
通过属性调用自动触发关联查询:
1 | public function get_Auth_msg() |
效果:
2. hasMany(一对多关系)
假设存在TpUserData(用户)、TpUserArticle(用户文章)、TpArticleTags(文章标签)三个模型:
- 一个用户可发表多篇文章(1:n)
- 一篇文章可包含多个标签(1:n)
方法定义
1 | hasMany('关联模型', '外键', '主键'); |
- 关联模型(必须):关联的模型类名
- 外键:关联模型中用于关联当前模型的字段(默认规则:当前模型名+_id)
- 主键:当前模型的主键(默认自动获取,可指定)
模型示例
1 | // TpArticleTags模型(文章标签,关联模型) |
1 | // TpUserArticle模型(文章,当前模型) |
测试示例
查询所有文章及关联数据:
1 | public function get_All_Articles() |
效果:
3. belongsTo(多对一关系:属于)
多对一关系表示“当前模型属于另一个模型”(如“多篇文章属于一个用户”)。
方法定义
1 | $this->belongsTo(关联模型类名, 外键, 关联模型主键); |
- 关联模型类名:被关联的模型(如用户模型
TpUserData::class) - 外键:当前模型中用于关联的字段(存储关联模型主键的值)
- 关联模型主键:关联模型中与外键匹配的主键(通常为
id)
模型示例
在TpUserArticle(文章)模型中,关联所属用户:
1 | /** |
测试示例
通过文章查询所属作者:
1 | public function get_Auth_msg() |
效果:
三种关联关系核心区别
| 关联方法 | 关联类型 | 适用场景 | 核心特征 |
|---|---|---|---|
hasOne |
一对一 | A模型与B模型一一对应(如用户-认证) | 主模型查子模型,返回单个结果 |
hasMany |
一对多 | A模型对应多个B模型(如用户-文章) | 主模型查子模型,返回多个结果(数据集) |
belongsTo |
多对一 | 多个B模型对应一个A模型(如文章-用户) | 子模型查主模型,返回单个结果(反向关联) |











