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
| $articles = Article::limit(10)->select();
foreach ($articles as $article) { $author = $article->author; }
|
有 with() 时(解决 N+1 问题):
1 2 3 4 5
| $articles = Article::with('author')->limit(10)->select(); foreach ($articles as $article) { $author = $article->author; }
|
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
| <?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 29 30 31
| <?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() { 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 19 20
| <?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 61 62 63 64 65
| <?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'); }
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); $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 28 29
| 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
|
public function author() { return $this->belongsTo(TpUserData::class, 'user_id', 'id'); }
|
3.2 模型字段筛选
当我们或许并不需要模型中的全部字段时候,那么我们就可以仅选择我们需要的哪些字段进行选择
1 2 3 4 5 6 7 8
|
public function author() { return $this->belongsTo(TpUserData::class, 'user_id', 'id') ->field('id, name, alias, email, avatar, create_time'); }
|
测试示例
通过文章查询所属作者:
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模型(如文章-用户) |
子模型查主模型,返回单个结果(反向关联) |