ThinkPHP 关联模型教程

ThinkPHP 的关联模型(也称为关联查询)是处理数据库表之间关系(如一对一、一对多、多对多)的核心功能,它允许你用非常优雅和面向对象的方式,在一次查询中获取到关联的数据,极大地简化了开发,并避免了“N+1 查询”的性能问题。

thinkphp 关联教程
(图片来源网络,侵删)

本教程将分为以下几个部分:

  1. 为什么需要关联模型?
  2. 核心概念:定义关联
  3. 关联类型详解
    • 一对一
    • 一对多
    • 多对多
  4. 关联查询的用法
    • 预查询
    • 链式查询
  5. 动态关联
  6. 最佳实践与注意事项

为什么需要关联模型?

假设我们有两个表:user(用户表)和 profile(用户资料表)。

user 表: | id | name | age | |----|------|-----| | 1 | 张三 | 25 | | 2 | 李四 | 30 |

profile 表: | id | user_id | address | phone | |----|---------|---------|-------| | 1 | 1 | 北京市朝阳区 | 138... | | 2 | 2 | 上海市浦东新区 | 139... |

thinkphp 关联教程
(图片来源网络,侵删)

不使用关联模型(传统方式):

// 1. 先查询用户
$user = User::find(1);
// 2. 再根据 user_id 查询资料
$profile = Profile::where('user_id', $user->id)->find();
// 3. 合并数据
$user->profile = $profile;

这种方式存在两个主要问题:

  • 性能问题:如果查询 100 个用户,就需要执行 1 + 100 = 101 次数据库查询,这就是著名的 “N+1 查询问题”
  • 代码冗余:每次需要获取关联数据时,都要写重复的查询代码。

使用关联模型:

ThinkPHP 允许你定义好模型之间的关系,然后通过一次查询就能获取所有数据。

thinkphp 关联教程
(图片来源网络,侵删)
// 在 User 模型中定义与 Profile 的关联
$user = User::with('profile')->find(1);
// 直接访问关联数据
echo $user->profile->address; // 输出: 北京市朝阳区

ThinkPHP 在底层会自动优化成 JOIN 查询或 IN 查询,只需 1-2 次数据库查询即可完成,性能和代码优雅度都得到了极大提升。


核心概念:定义关联

关联关系是在模型中通过特定的方法来定义的,这些方法通常以 hasOne, hasMany, belongsToMany 等命名。

定义关联时,你需要告诉 ThinkPHP:

  • 关联到哪个模型? (Profile)
  • 两个表通过什么字段关联? (通常是外键,如 user_id)
  • 当前模型在关系中扮演什么角色? (是“拥有”的一方,还是“属于”的一方)

关联类型详解

我们使用以下三个表作为示例:

  • user: 用户表
  • article: 文章表 (一个用户可以写多篇文章)
  • role: 角色表 (一个用户可以拥有多个角色,如“管理员”、“编辑”)

(1) 一对一

场景:一个用户只有一个详细资料。 定义:从当前模型(拥有方)出发,关联到另一个模型(属于方)。

示例UserProfile 的关系。

app/model/User.php

<?php
namespace app\model;
use think\Model;
class User extends Model
{
    // 定义与 Profile 模型的一对一关联
    // 一个 User 模型实例拥有一个 Profile 模型实例
    public function profile()
    {
        // 'Profile' 是关联的模型名
        // 'user_id' 是 Profile 表中关联到 User 表 id 的外键
        // 'id' 是当前 User 表的主键
        return $this->hasOne(Profile::class, 'user_id', 'id');
    }
}

app/model/Profile.php

<?php
namespace app\model;
use think\Model;
class Profile extends Model
{
    // 定义与 User 模型的反向关联(可选,但推荐)
    // 一个 Profile 模型实例属于一个 User 模型实例
    public function user()
    {
        return $this->belongsTo(User::class, 'user_id', 'id');
    }
}

查询方式

// 获取 ID 为 1 的用户及其资料
$user = User::with('profile')->find(1);
// 访问关联数据
if ($user && $user->profile) {
    echo "用户: " . $user->name . ", 地址: " . $user->profile->address;
}

(2) 一对多

场景:一个用户可以发表多篇文章。 定义:从当前模型(拥有方)出发,关联到另一个模型(属于方)的多个实例。

示例UserArticle 的关系。

app/model/User.php

<?php
namespace app\model;
use think\Model;
class User extends Model
{
    // 定义与 Article 模型的一对多关联
    // 一个 User 模型实例拥有多个 Article 模型实例
    public function articles()
    {
        // 'Article' 是关联的模型名
        // 'user_id' 是 Article 表中关联到 User 表 id 的外键
        // 'id' 是当前 User 表的主键
        return $this->hasMany(Article::class, 'user_id', 'id');
    }
}

app/model/Article.php

<?php
namespace app\model;
use think\Model;
class Article extends Model
{
    // 定义与 User 模型的反向关联(可选,但推荐)
    // 一个 Article 模型实例属于一个 User 模型实例
    public function author()
    {
        return $this->belongsTo(User::class, 'user_id', 'id');
    }
}

查询方式

// 获取 ID 为 1 的用户及其所有文章
$user = User::with('articles')->find(1);
// 遍历关联数据
foreach ($user->articles as $article) {
    echo "文章标题: " . $article->title . "<br>";
}

(3) 多对多

场景:一个用户可以拥有多个角色(如“管理员”、“编辑”),一个角色也可以被多个用户拥有。 定义:需要一张中间表(也叫“关联表”或“透视表”)来维护两个主表之间的关系。

表结构

  • user: id, name
  • role: id, name
  • user_role: user_id, role_id (中间表)

示例UserRole 的关系。

app/model/User.php

<?php
namespace app\model;
use think\Model;
class User extends Model
{
    // 定义与 Role 模型的多对多关联
    public function roles()
    {
        // 'Role' 是关联的模型名
        // 'user_role' 是中间表的表名
        // 'user_id' 是中间表中关联到 User 表 id 的字段
        // 'role_id' 是中间表中关联到 Role 表 id 的字段
        return $this->belongsToMany(Role::class, 'user_role', 'role_id', 'user_id');
    }
}

app/model/Role.php

<?php
namespace app\model;
use think\Model;
class Role extends Model
{
    // 定义与 User 模型的反向多对多关联
    public function users()
    {
        return $this->belongsToMany(User::class, 'user_role', 'user_id', 'role_id');
    }
}

查询方式

// 获取 ID 为 1 的用户及其所有角色
$user = User::with('roles')->find(1);
// 遍历关联数据
foreach ($user->roles as $role) {
    echo "角色: " . $role->name . "<br>";
}

关联查询的用法

定义好关联后,就可以在查询时使用了。

(1) 预查询

使用 with() 方法进行预查询,这是最常用、最高效的方式。

// 同时查询用户及其资料和文章
$user = User::with(['profile', 'articles'])->find(1);
// 指定关联表的字段
$user = User::with(['profile' => function($query) {
    $query->field('address, phone'); // 只查询 profile 表的 address 和 phone 字段
}])->find(1);
// 一对多关联,还可以对关联数据进行排序和筛选
$user = User::with(['articles' => function($query) {
    $query->where('status', 1)->order('create_time', 'desc');
}])->find(1);

(2) 链式查询

在链式查询中动态添加关联。

// 先构建查询,再添加关联
$user = User::where('age', '>', 20)->with('profile')->select();

动态关联

关联关系不是固定的,需要根据条件动态定义,可以使用 hasOne() 等方法的动态调用形式。

// 在 User 模型中
public function getProfileAttr($value, $data)
{
    // $data 是当前模型的数据
    if (isset($data['is_vip']) && $data['is_vip'] == 1) {
        // VIP 用户关联一个更详细的资料表
        return $this->hasOne(VipProfile::class, 'user_id', 'id');
    } else {
        // 普通用户关联普通资料表
        return $this->hasOne(Profile::class, 'user_id', 'id');
    }
}
// 查询时,ThinkPHP 会自动根据 is_vip 字段的值选择正确的关联
$user = User::with('profile')->find(1);

最佳实践与注意事项

  1. 模型文件必须存在:使用 with() 进行预查询时,关联的模型类(如 Profile)必须存在,否则会报错。
  2. 避免过度查询with() 会查询所有指定的关联数据,如果你只需要部分数据,务必在闭包中指定字段 (field()) 和条件 (where()),避免查询出不必要的数据,浪费资源。
  3. 反向关联的用途:反向关联(如 belongsTo)在“从属方”模型中非常有用,在 Article 模型中,你可以通过 $article->author->name 直接获取作者信息,而无需再手动查询 User 表。
  4. 中间表命名:多对多关联的中间表命名建议使用两个模型表名的单数形式按字母顺序排列并用下划线连接,如 user_role,这不是强制的,但遵循这个约定能让代码更清晰。
  5. 关联的惰性加载:如果你没有使用 with(),而是在获取到主模型后访问关联属性(如 $user->profile),ThinkPHP 会执行一次新的查询来获取关联数据,这会导致“N+1 问题”,应尽量避免。预查询 (with) 是首选方案。

ThinkPHP 的关联模型是一个强大而灵活的工具,掌握它,能让你在处理复杂业务逻辑时游刃有余。

核心要点回顾

  • 定义关系:在模型中通过 hasOne, hasMany, belongsToMany 等方法定义。
  • 查询数据:使用 with() 方法进行高效的预查询。
  • 理解角色:区分“拥有方”和“属于方”,正确选择 hasOne/hasManybelongsTo
  • 性能优化:始终在 with() 的闭包中指定查询条件,避免数据冗余。

希望这份教程对你有帮助!多加练习,你很快就能熟练运用 ThinkPHP 的关联功能。