
本文介绍如何将 Laravel 中多对多关联(如用户-角色)的嵌套查询结果,转换为“每个用户-角色组合独立成行”的扁平化结构,提供 Eloquent flatMap 方案与原生 SQL JOIN 两种高效实现方式。
本文介绍如何将 laravel 中多对多关联(如用户-角色)的嵌套查询结果,转换为“每个用户-角色组合独立成行”的扁平化结构,提供 eloquent `flatmap` 方案与原生 sql join 两种高效实现方式。
在 Laravel 应用中,当使用 belongsToMany 定义用户(User)与角色(Role)的多对多关系后,调用 User::with('roles')->get() 会返回类似以下结构的数据:每个 User 实例携带一个 Collection 类型的 roles 属性(可能含多个 Role 对象)。但某些场景(如前端表格渲染、导出 CSV、权限明细报表)要求「一个用户 + 一个角色」作为独立数据项——即一对多关系需被“爆炸展开”(explode),此时需将嵌套结构转为扁平化的一维集合。
✅ 方案一:Eloquent + flatMap()(推荐用于逻辑层控制)
利用 Laravel 集合的 flatMap() 方法,对每个用户及其角色进行映射与展开:
use Illuminate\Support\Facades\DB;
$userRoleFlattened = \App\Models\User::with('roles')->get()->flatMap(function ($user) {
// 若用户无角色,保留该用户(可选:也可跳过,用 `filter()` 处理)
if ($user->roles->isEmpty()) {
return $user;
}
// 为每个角色克隆一份用户,并只绑定当前角色
return $user->roles->map(function ($role) use ($user) {
$newUser = clone $user;
$newUser->setAttribute('role', $role); // 推荐:用新属性名避免覆盖原 roles 关系
// 或 $newUser->role_id = $role->id; $newUser->role_name = $role->name;
return $newUser;
});
});⚠️ 注意事项:
clone $user仅浅拷贝模型实例,不复制关联加载状态(如$user->roles仍存在,但建议重命名或清除以避免歧义);- 若需同时获取角色字段(如
role.name,role.slug),可在map()内显式设置属性,例如:$newUser->role_name = $role->name; $newUser->role_slug = $role->slug;- 此方案在内存中处理,适合中小数据量(
✅ 方案二:原生 JOIN 查询(高性能首选)
直接通过 SQL JOIN 将三表关联,在数据库层面完成扁平化,避免 N+1 与 PHP 内存膨胀:
use Illuminate\Support\Facades\DB;
$userRoleFlattened = DB::table('users')
->join('user_role', 'user_role.user_id', '=', 'users.id')
->join('roles', 'user_role.role_id', '=', 'roles.id')
->select('users.*', 'roles.id as role_id', 'roles.name as role_name', 'roles.slug as role_slug')
->get();该查询返回的是 stdClass 对象集合(非 Eloquent 模型),每条记录均为「用户字段 + 当前角色字段」的组合,天然满足“一用户一角色”结构。若需返回 User 模型实例并附加角色数据,可结合 from() 和 newQuery():
$userRoleModels = \App\Models\User::from('users')
->join('user_role', 'user_role.user_id', '=', 'users.id')
->join('roles', 'user_role.role_id', '=', 'roles.id')
->select('users.*', 'roles.id as role_id', 'roles.name as role_name')
->get()
->map(function ($item) {
$user = \App\Models\User::newFromBuilder((array) $item);
$user->role_id = $item->role_id;
$user->role_name = $item->role_name;
return $user;
});? 总结建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 数据量小、需复用模型方法/访问器 |
flatMap() + clone
|
保持 Eloquent 上下文,便于后续调用 $user->getFullName() 等逻辑 |
| 数据量大、追求性能与内存效率 | 原生 JOIN 查询 | 减少 PHP 层数据搬运,数据库直接聚合,支持分页、索引优化 |
| 需严格类型安全与 IDE 支持 | JOIN + map() 构造带角色属性的模型 |
兼顾性能与面向对象体验 |
无论采用哪种方式,最终均可获得形如 [ { "id": 1, "name": "Alice", "role_id": 2, "role_name": "Admin" }, { "id": 1, "name": "Alice", "role_id": 3, "role_name": "Editor" } ] 的扁平化结果,完美适配 API 响应、数据导出及前端展示需求。

















