一、先看完整 SQL(原样)
csharp
select m.*
from menu m
left join role_menu rm on rm.menu_id = m.id
where rm.role_id in (
select role_id from user_role where user_id = ?
)
order by m.parent_id, m.sort;
二、先搞清楚 4 张表在干什么(非常关键)
1️⃣ user(用户)
bash
user
┌────┬──────────┐
│ id │ username │
└────┴──────────┘
2️⃣ role(角色)
bash
role
┌────┬──────────┐
│ id │ role_code│
└────┴──────────┘
3️⃣ user_role(用户-角色关系)
一个用户可以有多个角色
user_role
┌────────┬────────┐
│ user_id│ role_id│
└────────┴────────┘
4️⃣ menu(菜单)
bash
menu
┌────┬────────┬──────────┬─────────┬──────┐
│ id │parent_id│ name │ path │ sort │
└────┴────────┴──────────┴─────────┴──────┘
parent_id用来表示菜单层级(父子菜单)
5️⃣ role_menu(角色-菜单关系)
角色能访问哪些菜单
role_menu
┌────────┬────────┐
│ role_id│ menu_id│
└────────┴────────┘
三、整体一句话解释这条 SQL 在干嘛
"查出某个用户 → 所有角色 → 能访问的所有菜单,并按菜单层级排序"
四、逐句拆解(重点)
🔹 ① 子查询:先查用户有哪些角色
csharp
select role_id from user_role where user_id = ?
假设:
ini
user_id = 10
user_role 表:
| user_id | role_id |
|---|---|
| 10 | 1 |
| 10 | 2 |
子查询结果:
scss
(1, 2)
👉 意思:这个用户有 2 个角色
🔹 ② where rm.role_id in (...)
bash
where rm.role_id in (1, 2)
👉 只要是角色 1 或 角色 2 拥有的菜单,都要
🔹 ③ left join role_menu
bash
left join role_menu rm on rm.menu_id = m.id
作用一句话:
把"菜单"和"角色-菜单关系"连起来
示例 role_menu 表:
| role_id | menu_id |
|---|---|
| 1 | 101 |
| 1 | 102 |
| 2 | 103 |
🔹 ④ select m.*
csharp
select m.*
👉 只要菜单字段,不关心 role_menu 的字段
最终你得到的,是:
css
menu 表中的数据
五、整体执行流程(一步一步)
🧠 数据库真实执行逻辑是这样的:
Step 1️⃣:查用户角色
ini
user_id = 10
↓
角色 = [1, 2]
Step 2️⃣:查这些角色能访问的菜单
css
role 1 → menu 101, 102
role 2 → menu 103
Step 3️⃣:合并去重后菜单
css
menu 101
menu 102
menu 103
Step 4️⃣:排序
vbnet
order by m.parent_id, m.sort
排序目的:
- 先按父菜单分组
- 再按菜单顺序排列
👉 方便前端直接生成树形菜单
六、为什么要用 LEFT JOIN 而不是 INNER JOIN?
实际上:
sql
LEFT JOIN + where rm.role_id in (...)
⬇️
效果等同于 INNER JOIN
那为什么很多人还写 LEFT JOIN?
1️⃣ 写法更通用
2️⃣ 以后可以扩展(比如允许空角色)
3️⃣ 历史代码习惯
👉 如果你愿意,完全可以改成:
csharp
select m.*
from menu m
inner join role_menu rm on rm.menu_id = m.id
where rm.role_id in (
select role_id from user_role where user_id = ?
)
order by m.parent_id, m.sort;
七、为什么不用三表直接 join?
你也可以写成:
vbnet
select distinct m.*
from user_role ur
join role_menu rm on rm.role_id = ur.role_id
join menu m on m.id = rm.menu_id
where ur.user_id = ?
order by m.parent_id, m.sort;
两种写法对比
| 写法 | 优点 |
|---|---|
| 子查询 in | 易读、好理解 |
| 多表 join | 性能稍好、专业 |
👉 中小项目两种都完全没问题
八、最终返回的数据长什么样?
css
[ { "id": 1, "parent_id": 0, "name": "系统管理", "path": "/system", "sort": 1 }, { "id": 2, "parent_id": 1, "name": "用户管理", "path": "/system/user", "sort": 1 }]
前端可以 直接递归生成菜单树
九、这条 SQL 在 RBAC 中的"地位"
👉 90% 的后台系统,菜单查询就是这一类 SQL
你学会这一条,就已经:
- ✔ 理解了 RBAC 的核心
- ✔ 能独立设计菜单权限
- ✔ 看得懂大多数后台源码