ThinkPHP6/8 模型关联高级用法:一对一、一对多、多对多关联实战
Orion K Lv6

ThinkPHP6/8的模型关联功能是ORM的核心特性之一,通过对象化的方式处理表与表之间的关联关系,相比传统的JOIN查询更加智能和高效。本文将深入探讨模型关联的高级用法、性能优化技巧和实战案例。

模型关联基础概念

关联类型概述

ThinkPHP支持以下几种关联类型:

  • hasOne:一对一关联
  • belongsTo:反向一对一关联
  • hasMany:一对多关联
  • belongsToMany:多对多关联
  • hasManyThrough:远程一对多关联
  • morphOne:多态一对一关联
  • morphMany:多态一对多关联

关联的优势

  1. 对象化操作:将表关联转换为对象属性访问
  2. 延迟加载:按需加载关联数据,提高性能
  3. 预载入:批量加载关联数据,减少SQL查询次数
  4. 智能缓存:自动处理关联数据缓存

一对一关联详解

基础用法

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
<?php
namespace app\model;

use think\Model;

/**
* 用户模型
*/
class User extends Model
{
/**
* 用户资料关联(一对一)
* @return \think\model\relation\HasOne
*/
public function profile()
{
// hasOne(关联模型, 外键, 主键)
return $this->hasOne(UserProfile::class, 'user_id', 'id');
}

/**
* 用户钱包关联(一对一)
* @return \think\model\relation\HasOne
*/
public function wallet()
{
return $this->hasOne(UserWallet::class, 'user_id', 'id');
}
}

/**
* 用户资料模型
*/
class UserProfile extends Model
{
/**
* 反向关联到用户
* @return \think\model\relation\BelongsTo
*/
public function user()
{
// belongsTo(关联模型, 外键, 主键)
return $this->belongsTo(User::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
24
25
26
27
28
29
30
/**
* 带条件的关联
*/
class User extends Model
{
/**
* 激活状态的用户资料
* @return \think\model\relation\HasOne
*/
public function activeProfile()
{
return $this->hasOne(UserProfile::class, 'user_id', 'id')
->where('status', 1)
->field('user_id,nickname,avatar,gender,birthday');
}

/**
* 带默认值的关联
* @return \think\model\relation\HasOne
*/
public function profileWithDefault()
{
return $this->hasOne(UserProfile::class, 'user_id', 'id')
->withDefault([
'nickname' => '未设置',
'avatar' => '/default/avatar.png',
'gender' => 0
]);
}
}

使用示例

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
// 控制器中的使用
class UserController
{
/**
* 获取用户详情(包含资料)
* @param int $id 用户ID
* @return array
*/
public function getUserDetail(int $id): array
{
// 方式1:延迟加载
$user = User::find($id);
$profile = $user->profile; // 这时才执行关联查询

// 方式2:预载入(推荐)
$user = User::with('profile')->find($id);

// 方式3:多个关联预载入
$user = User::with(['profile', 'wallet'])->find($id);

return [
'user' => $user->toArray(),
'profile' => $user->profile ? $user->profile->toArray() : null
];
}

/**
* 创建用户及资料
* @param array $userData 用户数据
* @param array $profileData 资料数据
* @return User
*/
public function createUserWithProfile(array $userData, array $profileData): User
{
// 开启事务
Db::startTrans();
try {
// 创建用户
$user = User::create($userData);

// 创建用户资料
$profileData['user_id'] = $user->id;
$user->profile()->save(new UserProfile($profileData));

Db::commit();
return $user;
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
}

一对多关联详解

基础配置

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
/**
* 用户模型
*/
class User extends Model
{
/**
* 用户订单关联(一对多)
* @return \think\model\relation\HasMany
*/
public function orders()
{
return $this->hasMany(Order::class, 'user_id', 'id');
}

/**
* 用户文章关联
* @return \think\model\relation\HasMany
*/
public function articles()
{
return $this->hasMany(Article::class, 'author_id', 'id');
}

/**
* 已发布的文章
* @return \think\model\relation\HasMany
*/
public function publishedArticles()
{
return $this->hasMany(Article::class, 'author_id', 'id')
->where('status', 'published')
->order('created_at desc');
}
}

/**
* 订单模型
*/
class Order extends Model
{
/**
* 反向关联到用户
* @return \think\model\relation\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}

/**
* 订单商品关联(一对多)
* @return \think\model\relation\HasMany
*/
public function items()
{
return $this->hasMany(OrderItem::class, 'order_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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class UserController
{
/**
* 获取用户及其订单统计
* @param int $userId 用户ID
* @return array
*/
public function getUserOrderStats(int $userId): array
{
$user = User::with([
// 预载入最近5个订单
'orders' => function($query) {
$query->order('created_at desc')->limit(5);
},
// 预载入订单商品
'orders.items'
])->find($userId);

// 统计数据
$stats = [
'total_orders' => $user->orders()->count(),
'total_amount' => $user->orders()->sum('total_amount'),
'pending_orders' => $user->orders()->where('status', 'pending')->count(),
'completed_orders' => $user->orders()->where('status', 'completed')->count(),
];

return [
'user' => $user->toArray(),
'stats' => $stats
];
}

/**
* 批量获取用户及订单数量
* @return array
*/
public function getUsersWithOrderCount(): array
{
// 使用withCount统计关联数量
$users = User::withCount(['orders', 'articles'])
->select()
->toArray();

return $users;
}
}

多对多关联详解

基础配置

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
/**
* 用户模型
*/
class User extends Model
{
/**
* 用户角色关联(多对多)
* @return \think\model\relation\BelongsToMany
*/
public function roles()
{
// belongsToMany(关联模型, 中间表, 外键, 关联键)
return $this->belongsToMany(Role::class, 'user_role', 'user_id', 'role_id')
->withPivot(['created_at', 'status']) // 获取中间表字段
->wherePivot('status', 1); // 中间表条件
}

/**
* 用户标签关联
* @return \think\model\relation\BelongsToMany
*/
public function tags()
{
return $this->belongsToMany(Tag::class, 'user_tag', 'user_id', 'tag_id')
->withPivot(['weight', 'created_at'])
->orderByPivot('weight', 'desc');
}
}

/**
* 角色模型
*/
class Role extends Model
{
/**
* 角色用户关联
* @return \think\model\relation\BelongsToMany
*/
public function users()
{
return $this->belongsToMany(User::class, 'user_role', 'role_id', 'user_id');
}

/**
* 角色权限关联
* @return \think\model\relation\BelongsToMany
*/
public function permissions()
{
return $this->belongsToMany(Permission::class, 'role_permission', 'role_id', 'permission_id');
}
}

中间表操作

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
class UserRoleController
{
/**
* 为用户分配角色
* @param int $userId 用户ID
* @param array $roleIds 角色ID数组
* @return bool
*/
public function assignRoles(int $userId, array $roleIds): bool
{
$user = User::find($userId);

// 方式1:直接关联
$user->roles()->attach($roleIds);

// 方式2:带中间表数据关联
$pivotData = [];
foreach ($roleIds as $roleId) {
$pivotData[$roleId] = [
'created_at' => date('Y-m-d H:i:s'),
'status' => 1
];
}
$user->roles()->attach($pivotData);

return true;
}

/**
* 同步用户角色
* @param int $userId 用户ID
* @param array $roleIds 角色ID数组
* @return bool
*/
public function syncRoles(int $userId, array $roleIds): bool
{
$user = User::find($userId);

// sync会删除不在数组中的关联,添加新的关联
$user->roles()->sync($roleIds);

return true;
}

/**
* 移除用户角色
* @param int $userId 用户ID
* @param array $roleIds 角色ID数组
* @return bool
*/
public function removeRoles(int $userId, array $roleIds): bool
{
$user = User::find($userId);

// 移除指定角色
$user->roles()->detach($roleIds);

// 移除所有角色
// $user->roles()->detach();

return true;
}
}

远程关联和多态关联

远程一对多关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 国家模型
*/
class Country extends Model
{
/**
* 国家的所有文章(通过用户)
* @return \think\model\relation\HasManyThrough
*/
public function articles()
{
// hasManyThrough(最终模型, 中间模型, 中间表外键, 最终表外键, 本表主键, 中间表主键)
return $this->hasManyThrough(
Article::class, // 最终要关联的模型
User::class, // 中间模型
'country_id', // 中间表(users)的外键
'author_id', // 最终表(articles)的外键
'id', // 本表(countries)的主键
'id' // 中间表(users)的主键
);
}
}

多态关联

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
/**
* 评论模型(多态)
*/
class Comment extends Model
{
/**
* 可评论的模型(多态关联)
* @return \think\model\relation\MorphTo
*/
public function commentable()
{
// morphTo(外键前缀)
return $this->morphTo('commentable');
}
}

/**
* 文章模型
*/
class Article extends Model
{
/**
* 文章评论(多态一对多)
* @return \think\model\relation\MorphMany
*/
public function comments()
{
// morphMany(关联模型, 多态名称)
return $this->morphMany(Comment::class, 'commentable');
}
}

/**
* 视频模型
*/
class Video extends Model
{
/**
* 视频评论(多态一对多)
* @return \think\model\relation\MorphMany
*/
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}

性能优化技巧

1. 预载入优化

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
class OptimizedController
{
/**
* 优化的用户列表查询
* @return array
*/
public function getUserList(): array
{
// 错误示例:N+1查询问题
// $users = User::select();
// foreach ($users as $user) {
// echo $user->profile->nickname; // 每次循环都会执行一次查询
// }

// 正确示例:使用预载入
$users = User::with([
'profile' => function($query) {
$query->field('user_id,nickname,avatar');
},
'roles' => function($query) {
$query->field('id,name,display_name');
}
])->select();

return $users->toArray();
}

/**
* 条件预载入
* @return array
*/
public function getActiveUsersWithOrders(): array
{
$users = User::with([
'orders' => function($query) {
// 只加载最近30天的订单
$query->where('created_at', '>=', date('Y-m-d', strtotime('-30 days')))
->field('id,user_id,total_amount,status,created_at')
->order('created_at desc');
}
])
->where('status', 'active')
->select();

return $users->toArray();
}
}

2. 关联查询缓存

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
/**
* 带缓存的用户模型
*/
class User extends Model
{
/**
* 缓存的用户资料关联
* @return \think\model\relation\HasOne
*/
public function cachedProfile()
{
return $this->hasOne(UserProfile::class, 'user_id', 'id')
->cache('user_profile_' . $this->id, 3600); // 缓存1小时
}

/**
* 获取用户权限(带缓存)
* @return array
*/
public function getPermissions(): array
{
$cacheKey = 'user_permissions_' . $this->id;

return cache($cacheKey, function() {
$permissions = [];
foreach ($this->roles as $role) {
foreach ($role->permissions as $permission) {
$permissions[] = $permission->name;
}
}
return array_unique($permissions);
}, 1800); // 缓存30分钟
}
}

3. 分页优化

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
class UserController
{
/**
* 优化的用户分页查询
* @param int $page 页码
* @param int $limit 每页数量
* @return array
*/
public function getUsersPaginated(int $page = 1, int $limit = 20): array
{
// 使用简单分页,避免count查询
$users = User::with([
'profile' => function($query) {
$query->field('user_id,nickname,avatar');
}
])
->field('id,username,email,status,created_at')
->order('id desc')
->paginate([
'list_rows' => $limit,
'page' => $page,
'simple' => true // 简单分页,不查询总数
]);

return $users->toArray();
}
}

实战案例:电商订单系统

模型设计

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
/**
* 订单模型
*/
class Order extends Model
{
/**
* 订单用户
* @return \think\model\relation\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}

/**
* 订单商品
* @return \think\model\relation\HasMany
*/
public function items()
{
return $this->hasMany(OrderItem::class, 'order_id', 'id');
}

/**
* 订单地址
* @return \think\model\relation\HasOne
*/
public function address()
{
return $this->hasOne(OrderAddress::class, 'order_id', 'id');
}

/**
* 订单支付记录
* @return \think\model\relation\HasMany
*/
public function payments()
{
return $this->hasMany(OrderPayment::class, 'order_id', 'id');
}
}

/**
* 订单商品模型
*/
class OrderItem extends Model
{
/**
* 关联商品
* @return \think\model\relation\BelongsTo
*/
public function product()
{
return $this->belongsTo(Product::class, 'product_id', 'id');
}

/**
* 关联商品SKU
* @return \think\model\relation\BelongsTo
*/
public function sku()
{
return $this->belongsTo(ProductSku::class, 'sku_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
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class OrderService
{
/**
* 获取订单详情
* @param int $orderId 订单ID
* @return array
*/
public function getOrderDetail(int $orderId): array
{
$order = Order::with([
'user:id,username,nickname',
'items.product:id,name,image',
'items.sku:id,name,price',
'address',
'payments' => function($query) {
$query->where('status', 'success');
}
])->find($orderId);

if (!$order) {
throw new \Exception('订单不存在');
}

return [
'order' => $order->toArray(),
'total_paid' => $order->payments->sum('amount'),
'item_count' => $order->items->sum('quantity')
];
}

/**
* 创建订单
* @param array $orderData 订单数据
* @return Order
*/
public function createOrder(array $orderData): Order
{
Db::startTrans();
try {
// 创建订单
$order = Order::create([
'order_no' => $this->generateOrderNo(),
'user_id' => $orderData['user_id'],
'total_amount' => $orderData['total_amount'],
'status' => 'pending'
]);

// 创建订单商品
$items = [];
foreach ($orderData['items'] as $item) {
$items[] = new OrderItem([
'product_id' => $item['product_id'],
'sku_id' => $item['sku_id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
'total_price' => $item['quantity'] * $item['price']
]);
}
$order->items()->saveAll($items);

// 创建订单地址
$order->address()->save(new OrderAddress($orderData['address']));

Db::commit();
return $order;
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}

/**
* 生成订单号
* @return string
*/
private function generateOrderNo(): string
{
return date('YmdHis') . mt_rand(1000, 9999);
}
}

最佳实践总结

1. 关联设计原则

  • 明确关联关系:准确定义表之间的关联类型
  • 合理设置外键:遵循数据库设计规范
  • 避免过深嵌套:关联层级不宜超过3层
  • 使用软删除:重要数据使用软删除机制

2. 性能优化建议

  • 预载入优先:避免N+1查询问题
  • 字段筛选:只查询需要的字段
  • 适当缓存:对频繁查询的关联数据进行缓存
  • 分页处理:大数据量使用分页查询

3. 代码规范

  • 统一命名:关联方法使用驼峰命名
  • 添加注释:为关联方法添加详细注释
  • 异常处理:合理处理关联查询异常
  • 事务控制:复杂操作使用数据库事务

4. 调试技巧

1
2
3
4
5
6
7
8
9
10
11
12
// 开启SQL日志
Db::listen(function($sql, $time, $explain){
echo $sql . ' [' . $time . 's]' . PHP_EOL;
});

// 查看关联查询SQL
$user = User::with('profile');
echo $user->getLastSql(); // 查看最后执行的SQL

// 调试关联数据
$user = User::find(1);
dump($user->profile); // 查看关联数据结构

ThinkPHP的模型关联功能强大且灵活,合理使用可以大大提高开发效率和代码质量。在实际项目中,应该根据业务需求选择合适的关联类型,并注意性能优化,避免常见的查询陷阱。

本站由 提供部署服务