多对多关联是数据库设计中相对复杂的关系,例如「用户和角色」:一个用户可以拥有多个角色,一个角色也可以被多个用户拥有。
Laravel 的 Eloquent ORM 通过 belongsToMany 方法提供了简洁优雅的多对多关联定义和操作方式。
本章将详细介绍如何定义和使用多对多关联。
多对多关联表示两个模型之间可以互相拥有多个对方。例如:
在关系型数据库中,多对多关联需要一张「中间表」(也称为枢轴表),用于记录两个模型之间的关联关系。
users表
- id
- name
- email
roles表
- id
- name
- slug
role_user表 (中间表,按字母顺序命名)
- user_id
- role_id
- created_at (可选)
- updated_at (可选)
使用 belongsToMany 方法定义多对多关联,该方法返回 Illuminate\Database\Eloquent\Relations\BelongsToMany 实例。
在 User 模型中定义与 Role 的多对多关联:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class);
}
}
在 Role 模型中定义反向关联:
class Role extends Model
{
public function users()
{
return $this->belongsToMany(User::class);
}
}
如果中间表名不是默认的(按字母顺序连接两个模型名),可以自定义:
return $this->belongsToMany(Role::class, 'user_role');
如果中间表的外键或关联键不是默认的,可以分别指定:
return $this->belongsToMany(Role::class, 'user_role', 'user_id', 'role_id');
如果中间表除了外键外还有其他字段(如创建时间、附加属性),可以用 withPivot 方法:
return $this->belongsToMany(Role::class)
->withPivot('created_at', 'expires_at');
如果中间表包含时间戳字段,可以用 withTimestamps:
return $this->belongsToMany(Role::class)->withTimestamps();
User 和 Role 生成 role_user。如果你希望使用其他名称,请显式指定。
定义好关联后,可以方便地获取、添加、更新和删除关联数据。
$user = User::find(1);
$roles = $user->roles; // 集合
foreach ($roles as $role) {
echo $role->name;
}
使用 attach 方法将模型关联起来:
// 添加单个角色
$user->roles()->attach($roleId);
// 添加多个角色
$user->roles()->attach([1, 2, 3]);
// 同时设置中间表字段
$user->roles()->attach($roleId, ['expires_at' => '2025-12-31']);
// 移除单个角色
$user->roles()->detach($roleId);
// 移除多个角色
$user->roles()->detach([1, 2, 3]);
// 移除所有角色
$user->roles()->detach();
sync 方法会同步关联数组,只保留数组中指定的 ID,其他将被移除:
$user->roles()->sync([1, 2, 3]); // 用户只拥有角色1,2,3
// 同时传递中间表字段
$user->roles()->sync([
1 => ['expires_at' => '2025-01-01'],
2 => ['expires_at' => '2025-06-01'],
]);
如果不想移除未指定的关联,可以使用 syncWithoutDetaching:
$user->roles()->syncWithoutDetaching([1, 2]); // 添加角色1和2,但保留原有其他角色
toggle 方法会添加不存在的关联,并移除已存在的关联:
$user->roles()->toggle([1, 2, 3]);
$user->roles()->updateExistingPivot($roleId, ['expires_at' => '2026-01-01']);
当访问关联时,中间表数据可以通过 pivot 属性访问:
$user = User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at; // 如果中间表有 created_at 字段
}
注意:只有通过 withPivot 声明的字段才能被访问。
你可以给中间表自定义一个名称(如 as('membership'))来替代 pivot:
return $this->belongsToMany(Role::class)->as('membership')->withTimestamps();
然后通过 $role->membership->created_at 访问。
$users = User::whereHas('roles', function ($query) {
$query->where('name', 'admin');
})->get();
$users = User::with('roles')->get();
foreach ($users as $user) {
foreach ($user->roles as $role) {
echo $role->name;
}
}
预加载时也可以过滤:
$users = User::with(['roles' => function ($query) {
$query->where('active', true);
}])->get();
// 用户 -> 角色 -> 权限(假设角色有多对多权限)
$users = User::with('roles.permissions')->get();
下面是一个完整的用户角色管理示例。
模型定义:
// User.php
public function roles()
{
return $this->belongsToMany(Role::class)
->withPivot('expires_at')
->withTimestamps();
}
// Role.php
public function users()
{
return $this->belongsToMany(User::class)
->withPivot('expires_at')
->withTimestamps();
}
使用示例:
// 给用户分配角色
$user->roles()->attach($roleId, ['expires_at' => now()->addYear()]);
// 获取用户所有角色,并查看过期时间
$user = User::with('roles')->find(1);
foreach ($user->roles as $role) {
echo "角色: {$role->name}, 过期时间: {$role->pivot->expires_at}";
}
// 同步角色,并设置每个角色的过期时间
$user->roles()->sync([
1 => ['expires_at' => '2025-01-01'],
2 => ['expires_at' => '2025-06-01'],
]);
// 获取所有拥有 'admin' 角色的用户
$admins = User::whereHas('roles', function ($query) {
$query->where('name', 'admin');
})->get();
// 获取某个角色的所有用户,并按用户名排序
$role = Role::where('name', 'editor')->first();
$users = $role->users()->orderBy('name')->get();
sync 方法时注意性能,因为它会执行删除和插入操作。对于大量数据,考虑使用 attach 和 detach 分批处理。
如果中间表需要更多的业务逻辑或事件,可以创建一个中间表模型,并在关联中使用:
// 创建中间表模型 UserRole
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserRole extends Pivot
{
// 自定义逻辑
}
// 在关联中使用
return $this->belongsToMany(Role::class)->using(UserRole::class);
如果使用自定义中间表模型,可以在其中定义访问器和修改器。
$user->roles()->detach($roleId);
使用 newPivotQuery 获取中间表的查询构建器:
$user->roles()->newPivotQuery()->where('expires_at', '<', now())->delete();
User 和 Role 生成 role_user,Post 和 Tag 生成 post_tag。
withPivot('field1', 'field2') 声明额外字段,然后在 attach、sync 等操作时传递这些字段的值。
sync 会移除所有不在给定数组中的关联,只保留数组中的。而 syncWithoutDetaching 仅添加新的关联,不会移除已有的。
多对多关联是 Eloquent 中非常强大的特性,通过 belongsToMany 方法可以轻松定义,并利用 attach、detach、sync 等方法管理关联。
掌握中间表操作和预加载技巧,可以让复杂的关系处理变得简单高效。
下一章我们将学习模型的其他高级特性,如访问器、修改器和事件。