Laravel:indexQuery与筛选器叠加导致role_id重复IN的问题解决
问题场景
我在Laravel中自定义了indexQuery,代码如下:
$resourceTable = 'soft'; $user = Auth::user(); $orderBy = $request->get('orderBy'); $dir = $request->get('orderByDirection'); $query->select(DB::raw("{$resourceTable}.*")); if (!$user->isGlobalAdmin) { $roles = json_decode($user->getRoleNames()); $rolesArray = []; foreach ($roles as $roleName) { $role = json_decode(Role::findByName($roleName)); $rolesArray[] = $role->id; } $query->join('model_has_roles', 'model_id', "=", "{$resourceTable}.id") ->where('model_type', '=', 'App\Models\Soft') ->whereIn('role_id', $rolesArray); }
加载资源列表页时,生成的SQL为:
select soft.* from `soft` inner join `model_has_roles` on `model_id` = `soft`.`id` where `model_type` = 'App\\Models\\Soft' and `role_id` in (2, 3)
同时设置了角色筛选器,其apply代码如下:
$roles = $value; $rolesArray = []; foreach ($roles as $roleName) { $role = json_decode(Role::findByName($roleName)); $rolesArray[] = $role->id; } return $query->whereIn('role_id', $rolesArray);
当选择单个角色进行筛选时,生成的SQL出现了两个role_id IN条件:
select soft.* from `soft` inner join `model_has_roles` on `model_id` = `soft`.`id` where `role_id` in (3) and `model_type` = App\\Models\\Soft and `role_id` in (2, 3)
需要修改代码,让SQL中只保留一个role_id IN条件。
解决方案
核心思路是合并用户默认的角色范围和筛选器的角色范围,而非追加重复条件,以下是两种可行实现方式:
方式一:在筛选器中直接合并角色集合
修改筛选器的apply方法,先获取用户自身的角色ID集合,再和筛选的角色ID取交集(非全局管理员场景),最后替换原有的role_id条件:
$roles = $value; $user = Auth::user(); $filterRoleIds = []; foreach ($roles as $roleName) { // 无需json_decode,直接取Eloquent模型的ID $role = Role::findByName($roleName); $filterRoleIds[] = $role->id; } if (!$user->isGlobalAdmin) { // 获取用户自身的角色ID集合 $userRoles = json_decode($user->getRoleNames()); $userRoleIds = []; foreach ($userRoles as $roleName) { $role = Role::findByName($roleName); $userRoleIds[] = $role->id; } // 取交集,确保筛选结果在用户权限范围内 $finalRoleIds = array_intersect($userRoleIds, $filterRoleIds); // 移除原有role_id的In条件 $query->getQuery()->wheres = collect($query->getQuery()->wheres) ->filter(fn($where) => !($where['type'] === 'In' && $where['column'] === 'role_id')) ->values() ->all(); // 添加合并后的条件 if (!empty($finalRoleIds)) { $query->whereIn('role_id', $finalRoleIds); } } else { // 全局管理员直接使用筛选的角色 $query->whereIn('role_id', $filterRoleIds); } return $query;
方式二:在indexQuery中存储角色范围,筛选器复用
先在indexQuery里把用户的角色ID集合存为查询的自定义属性,再在筛选器中直接合并:
- 修改
indexQuery代码:
$resourceTable = 'soft'; $user = Auth::user(); $orderBy = $request->get('orderBy'); $dir = $request->get('orderByDirection'); $query->select(DB::raw("{$resourceTable}.*")); if (!$user->isGlobalAdmin) { $roles = json_decode($user->getRoleNames()); $userRoleIds = []; foreach ($roles as $roleName) { $role = Role::findByName($roleName); $userRoleIds[] = $role->id; } // 将用户角色ID存为查询的自定义属性 $query->userRoleIds = $userRoleIds; $query->join('model_has_roles', 'model_id', "=", "{$resourceTable}.id") ->where('model_type', '=', 'App\Models\Soft') ->whereIn('role_id', $userRoleIds); }
- 修改筛选器的
apply方法:
$roles = $value; $filterRoleIds = []; foreach ($roles as $roleName) { $role = Role::findByName($roleName); $filterRoleIds[] = $role->id; } // 检查是否存在用户角色范围限制 if (property_exists($query, 'userRoleIds')) { // 合并用户角色与筛选角色的交集 $finalRoleIds = array_intersect($query->userRoleIds, $filterRoleIds); // 移除原有role_id的In条件 $query->getQuery()->wheres = collect($query->getQuery()->wheres) ->filter(fn($where) => !($where['type'] === 'In' && $where['column'] === 'role_id')) ->values() ->all(); if (!empty($finalRoleIds)) { $query->whereIn('role_id', $finalRoleIds); } } else { $query->whereIn('role_id', $filterRoleIds); } return $query;
额外优化建议
- 避免重复查询角色:可以在用户模型中添加访问器,直接返回角色ID集合,或者缓存查询结果,减少数据库请求。
- 移除不必要的
json_decode:Role::findByName返回的是Eloquent模型对象,直接调用$role->id即可,无需转JSON再解码。
内容的提问来源于stack exchange,提问作者Maksim Riabchenko
相关产品推荐
相关产品推荐

