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

ThinkPHP中hasOne、hasMany和belongsTo的关联关系详解

本文的一个概念

在ThinkPHP框架中,hasOnehasManybelongsTo用于定义模型间的关联关系,分别对应一对一(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. 先查询主模型列表:执行 1 次查询获取主模型的 N 条记录(如“查询所有文章”)。
  2. 循环中查询关联数据:遍历这 N 条记录,对每条记录单独执行 1 次查询获取关联模型数据(如“循环每篇文章,查询其作者”)。

形成条件:

  • 需要查询 主模型的多条记录(N ≥ 1);
  • 每条主模型记录都需要 关联查询其他模型的数据
  • 未使用 关联预加载(如 with() 方法),而是采用“即时加载”(访问关联属性时才触发查询)。

三、N+1 查询的影响

  1. 性能急剧下降:数据库查询是 Web 应用中最耗时的操作之一。N 越大(如 N=1000),额外产生的 N 次查询会导致响应时间大幅增加(1001 次查询 vs 2 次查询)。
  2. 数据库压力增大:频繁的查询请求会占用数据库连接资源,可能导致连接池耗尽、并发能力下降。
  3. 代码效率低下:循环中嵌套查询的逻辑会让代码执行效率降低,尤其在数据量较大时,问题会被放大。

四、with() 方法如何解决 N+1 查询问题?

with() 方法的核心是 “关联预加载”:在查询主模型数据的同时,一次性批量查询所有关联模型的数据,再通过程序逻辑将关联数据与主模型记录“匹配”,最终只需要 2 次查询(主模型 1 次 + 关联模型 1 次),彻底避免 N 次额外查询。

以本文的代码实例:

假设你的文章模型(Article)通过 author() 方法关联用户模型(TpUserData),现在要查询 10 篇文章及其作者:

  1. 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)
  2. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TpUserAuthentication模型(关联模型)
<?php
namespace app\model;

use think\Model;

class TpUserAuthentication extends Model
{
protected $pk = 'id'; // 主键
protected $schema = [
'id' => 'VARCHAR(100)',
'authorization' => 'VARCHAR(500)',
'status' => 'TINYINT',
'common_note' => 'text',
'role' => 'enum("28.7博主","用户","游客")',
'create_time' => 'TIMESTAMP',
'update_time' => 'TIMESTAMP',
];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// TpUserData模型(当前模型)
<?php
namespace app\model;

use think\Model;

class TpUserData extends Model
{
protected $pk = 'id'; // 主键
protected $schema = [
'id' => 'VARCHAR(100)',
'name' => 'VARCHAR(100)',
'alias' => 'VARCHAR(100)',
'email' => 'VARCHAR(100)',
'avatar' => 'VARCHAR(100)',
'password' => 'VARCHAR(500)',
'tattle' => 'text',
'create_time' => 'TIMESTAMP',
'update_time' => 'TIMESTAMP',
];

// 定义与认证信息的一对一关联
public function authorization()
{
// 参数:关联模型类名、关联模型外键(TpUserAuthentication.id)、当前模型主键(TpUserData.id)
return $this->hasOne('TpUserAuthentication', 'id', 'id');
}
}

测试示例

通过属性调用自动触发关联查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function get_Auth_msg()
{
$id = Request()->param("id");

$user = TpUserData::find($id);
if (!$user) {
return json([
'success' => false,
'code' => 404,
'message' => '用户不存在'
]);
}

// 自动查询关联的认证信息
$authData = $user->authorization;

return json([
'success' => true,
'code' => 200,
'message' => "",
'data' => $authData
]);
}

效果:

2. hasMany(一对多关系)

假设存在TpUserData(用户)、TpUserArticle(用户文章)、TpArticleTags(文章标签)三个模型:

  • 一个用户可发表多篇文章(1:n)
  • 一篇文章可包含多个标签(1:n)

方法定义

1
hasMany('关联模型', '外键', '主键');
  • 关联模型(必须):关联的模型类名
  • 外键:关联模型中用于关联当前模型的字段(默认规则:当前模型名+_id)
  • 主键:当前模型的主键(默认自动获取,可指定)

模型示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// TpArticleTags模型(文章标签,关联模型)
<?php
namespace app\model;

use think\Model;

class TpArticleTags extends Model
{
protected $schema = [
'user_id' => 'VARCHAR(100)',
'art_id' => 'VARCHAR(255)',
'tag_1' => 'VARCHAR(255)',
'tag_2' => 'VARCHAR(255)',
'tag_3' => 'VARCHAR(255)',
'create_time' => 'TIMESTAMP',
'update_time' => 'TIMESTAMP',
];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// TpUserArticle模型(文章,当前模型)
<?php
namespace app\model;

use think\Model;

class TpUserArticle extends Model
{
protected $pk = 'art_id'; // 主键
protected $schema = [
'user_id' => 'VARCHAR(100)',
'art_id' => 'VARCHAR(255)',
'title' => 'TEXT',
'categories' => 'VARCHAR(255)',
'cover' => 'VARCHAR(255)',
'create_time' => 'TIMESTAMP',
'update_time' => 'TIMESTAMP',
];

// 定义与标签的一对多关联(一篇文章多个标签)
public function article_tags()
{
return $this->hasMany(TpArticleTags::class, 'art_id', 'art_id');
}

// 定义与作者的多对一关联(见belongsTo部分)
public function author()
{
return $this->belongsTo(TpUserData::class, 'user_id', 'id');
}

// 模型方法:查询所有文章(含关联数据)
public static function getAllArticles(
array $where = [],
array $order = ['create_time' => 'desc'],
bool $withTags = true,
int $page = 0,
int $limit = 10
) {
$query = self::where($where);

// 预加载关联模型(避免N+1查询问题)
$relations = ['author'];
if ($withTags) {
$relations[] = 'articleTags';
}
$query->with($relations);
$query->order($order);

// 分页或全量查询
if ($page > 0) {
return $query->paginate([
'page' => $page,
'list_rows' => $limit
]);
} else {
return $query->select();
}
}
}

测试示例

查询所有文章及关联数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public function get_All_Articles()
{
$where = Request()->param('where', [], 'trim');
$keyword = Request()->param('keyword', '', 'trim');
$page = Request()->param('page', 1, 'intval');
$limit = Request()->param('limit', 10, 'intval');
$withTags = Request()->param('withTags', true, 'boolval');
$field = Request()->param('field', '*', 'trim');

// 调用模型方法查询
$articles = TpUserArticle::getAllArticles(
$where,
['create_time' => 'desc'],
$withTags,
$page,
$limit,
$field,
$keyword
);

return json([
'success' => true,
'code' => 200,
'data' => $articles,
'total' => $page > 0 ? $articles->total() : count($articles)
]);
}

效果:

3. belongsTo(多对一关系:属于)

多对一关系表示“当前模型属于另一个模型”(如“多篇文章属于一个用户”)。

方法定义

1
$this->belongsTo(关联模型类名, 外键, 关联模型主键);
  • 关联模型类名:被关联的模型(如用户模型TpUserData::class
  • 外键:当前模型中用于关联的字段(存储关联模型主键的值)
  • 关联模型主键:关联模型中与外键匹配的主键(通常为id

模型示例

TpUserArticle(文章)模型中,关联所属用户:

1
2
3
4
5
6
7
8
/**
* 关联作者(多对一:文章属于用户)
* 文章表的user_id → 用户表的id
*/
public function author()
{
return $this->belongsTo(TpUserData::class, 'user_id', 'id');
}

测试示例

通过文章查询所属作者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function get_Auth_msg()
{
$art_id = Request()->param("art_id");

$article = TpUserArticle::find($art_id);
if (!$article) {
return json([
'success' => false,
'code' => 404,
'message' => '文章不存在'
]);
}

// 自动查询关联的作者信息
$authorData = $article->author;

return json([
'success' => true,
'code' => 200,
'message' => "",
'data' => $authorData
]);
}

效果:

三种关联关系核心区别

关联方法 关联类型 适用场景 核心特征
hasOne 一对一 A模型与B模型一一对应(如用户-认证) 主模型查子模型,返回单个结果
hasMany 一对多 A模型对应多个B模型(如用户-文章) 主模型查子模型,返回多个结果(数据集)
belongsTo 多对一 多个B模型对应一个A模型(如文章-用户) 子模型查主模型,返回单个结果(反向关联)