RBAC 菜单查询的“标准写法”

一、先看完整 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│
└────────┴────────┘

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 的核心
  • ✔ 能独立设计菜单权限
  • ✔ 看得懂大多数后台源码
相关推荐
呆萌很4 小时前
MySQL 8.4 LTS 完整安装教程
数据库·mysql
Thomas21437 小时前
mysql 索引面试复习
数据库·mysql·面试
小袁拒绝摆烂8 小时前
一条 SQL 从 30 秒到 300 毫秒:聊聊 MySQL 的 Hash Join
sql·mysql·哈希算法
敲代码的嘎仔9 小时前
自己设计了一个兑换码算法:自增ID + Base32转码 + 按位加权签名 + 异或混淆,面试被追问细节时终于不用慌了
java·数据库·mysql·算法·微服务·面试·职场和发展
—Miss. Z—10 小时前
计算机三级数据库技术—应用题2️⃣
数据库·mysql
这个DBA有点耶11 小时前
临时表从4.7秒到0.12秒:不是所有Using temporary都需要优化
数据库·mysql·代码规范
天衍四九-13 小时前
MySQL有哪些索引
数据库·mysql
kyrie_sakura15 小时前
MySQL学习笔记4 -- select的7大子句,子查询
笔记·学习·mysql
snow@li16 小时前
服务器运维:mysql 安装笔记
运维·服务器·mysql