用户 (User) → 角色 (Role) → 菜单/权限 (Menu/Permission)
一、权限需要哪些字段
基础字段:
- id
- name
- status
- remark
关联字段:
- menu
- user
基础字段的 CRUD 不过多介绍,主要了解一下 中间表、关联字段; 先看看两个实体中如何定义关系的:
二、创建角色
menu.entity.ts
typescript
import { Column, Entity, Index, ManyToMany, ManyToOne, OneToMany, Relation } from 'typeorm'
import { CommonEntity } from '~/common/entity/common.entity'
import { RoleEntity } from '../../role/entities/role.entity'
import { MenuType } from '../dto/menu.dto'
@Entity('sys_menu')
export class MenuEntity extends CommonEntity {
// ...
@ManyToMany(() => RoleEntity, role => role.menus, {
onDelete: 'CASCADE',
})
roles: Relation<RoleEntity[]>
}
role.entity.ts
less
import { Column, Entity, Index, ManyToMany, JoinTable, Relation } from 'typeorm'
import { CommonEntity } from '~/common/entity/common.entity'
import { MenuEntity } from '../../menu/entities/menu.entity'
import { UserEntity } from '~/modules/users/entities/user.entity'
@Entity('sys_role')
export class RoleEntity extends CommonEntity {
// ...
@ManyToMany(() => MenuEntity, menu => menu.roles, {})
@JoinTable({
name: 'sys_role_menus',
joinColumn: { name: 'role_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'menu_id', referencedColumnName: 'id' },
})
menus: Relation<MenuEntity[]>
}
role实体中 menus 字段上有 @JoinTable,所以它是关系拥有方。
疑问
一、在创建角色时,如何更新中间表?
先看一下创建角色的实现代码:
kotlin
/**
* 创建角色
* - `code` / `name` 任一重复都拒绝创建(对应实体唯一索引)
*/
async create({ menuIds, ...data }: CreateRoleDto) {
const exists = await this.roleRepository.findOne({
where: [
{ code: data.code },
{ name: data.name },
],
})
if (exists)
throw new HttpException('角色编码或名称已存在', 400)
const entity = this.roleRepository.create({
...data,
})
if (menuIds?.length) {
const menus = await this.menuRepository.findBy({ id: In(menuIds) })
if (menus.length !== menuIds.length)
throw new HttpException('部分菜单不存在', 400)
entity.menus = menus
}
const role = await this.roleRepository.save(entity)
return { roleId: role.id }
}
创建角色,需要传递角色的基本信息和菜单ids,然后通过 menuIds 去数据库中查处相关数据,组装成一个新的 role实体;
save方法将新建的 role实体 存入数据库中;
这个过程中:
- 先创建 role
- 然后获取 roleId,更新中间表
sys_role_menus,将roleId 和 menuId 一一对应
所以:
save 时不能直接写 menus: menuIds(那是数字数组),需要 MenuEntity[]。所以先用 findBy 把 ID 转成实体,TypeORM 再在 sys_role_menus 里写入关联。
- 有
menuIds→ 查出对应菜单并关联 - 没有
menuIds→menus: [],不关联任何菜单
自动更新中间表,不需要手动去维护!!
三、获取角色详情
- 返回角色信息和关联菜单 ID 列表
- 若角色不存在,则抛出 404 错误
- 若角色存在,则返回角色信息和关联菜单 ID 列表
csharp
async findOne(id: number) {
const role = await this.roleRepository.findOne({ where: { id } })
if (!role)
throw new HttpException('角色不存在', 404)
// 查出所有关联了该角色的菜单
const menus = await this.menuRepository.find({
where: { roles: { id } },
select: ['id'],
})
return { ...role, menuIds: menus.map(m => m.id) }
}
this.menuRepository.find(...)用 TypeORM 的MenuEntity仓库查sys_menu表,返回的是菜单对象数组。where: { roles: { id } }按关联关系过滤,不是按菜单自己的id。
roles 是 MenuEntity 上的多对多字段,对应中间表 `sys_role_menus``
{ id } 是 { id: id } 的简写,这里的 id 来自外层 findOne(id: number) 的参数,即角色 id
含义:查出所有关联了该角色的菜单。
-
select: ['id']只查
id列,不查title、path、component等。返回结果类似:
bash[ { id: 1 }, { id: 5 }, { id: 8 }, ]
疑问:
为什么在数据库中, sys_menu 表中并没有展示|看到 roles 字段?
因为 roles 不是 sys_menu 表里的真实列,而是 TypeORM 在代码里定义的关联属性,用来表示「这个菜单关联了哪些角色」。
实体 vs 数据库
在 MenuEntity 里:
kotlin
@ManyToMany(() => RoleEntity, role => role.menus, {
onDelete: 'CASCADE',
})
roles: Relation<RoleEntity[]>
这里的 roles 带有 @ManyToMany,表示多对多关系,不会映射成 sys_menu 上的一列。
你在数据库里看到的 sys_menu,通常只有菜单自己的字段,例如:
idparent_idtitlepathcomponent- ...
不会有 roles 列,这是正常的。
关系存在哪里?
角色和菜单是多对多,实际数据放在中间表里。你们项目里在 RoleEntity 一侧定义了:
role.entity.ts
less
@ManyToMany(() => MenuEntity, menu => menu.roles, {})
@JoinTable({
name: 'sys_role_menus',
joinColumn: { name: 'role_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'menu_id', referencedColumnName: 'id' },
})
menus: Relation<MenuEntity[]>
也就是说:
| 表 | 作用 |
|---|---|
sys_menu |
存菜单本身 |
sys_role |
存角色本身 |
sys_role_menus |
存「哪个角色 ↔ 哪个菜单」的对应关系 |
中间表大致长这样:
| role_id | menu_id |
|---|---|
| 1 | 3 |
| 1 | 5 |
| 2 | 3 |
一条记录表示:角色 1 拥有菜单 3 的权限。
那代码里的 where: { roles: { id } } 是什么意思?
这是 TypeORM 的关联查询语法,不是去查 sys_menu.roles 这一列。
它会被翻译成类似这样的 SQL:
ini
FROM sys_menu menu
INNER JOIN sys_role_menus rm ON rm.menu_id = menu.id
WHERE rm.role_id = :id
所以:
roles:代码里的关系名,方便 ORM 写查询、加载关联数据sys_role_menus:数据库里真正存关系的地方
类比理解
可以把它想成:
sys_menu= 菜单清单sys_role= 角色清单sys_role_menus= 勾选表(谁勾选了哪些菜单)
勾选关系不会写进菜单表的一列里,而是单独一张关联表。
总结:在数据库里看不到 roles 字段是预期行为;要看角色和菜单的绑定关系,请查 sys_role_menus 表,而不是在 sys_menu 里找 roles 列。
更新角色
-
传递参数:
bashparams: { id, { menuIds, ...data } } -
通过id查询 角色是否存在,不存在返回 404
-
通过id查询出 role 实体,通过data 更新角色信息
-
通过munuIds 更新绑定的 menu 信息
-
最后保存 新的 role
-
建议:通过事务完成以上操作
csharp
async update(id, { menuIds, ...data }: RoleUpdateDto): Promise<void> {
const role = await this.roleRepository.findOne({ where: { id } })
if (!role)
throw new HttpException('角色不存在', 404)
await this.roleRepository.update(id, data)
await this.entityManager.transaction(async (manager) => {
const role = await this.roleRepository.findOne({ where: { id } })
role.menus = menuIds?.length
? await this.menuRepository.findBy({ id: In(menuIds) })
: []
await manager.save(role)
})
}
删除角色
typescript
async delete(id: number): Promise<void> {
if (id === ROOT_ROLE_ID)
throw new Error('不能删除超级管理员')
await this.roleRepository.delete(id)
}
roleRepository.delete(id) 实际做了什么
repository.delete(id) 只会执行类似:
sql
DELETE FROM sys_role WHERE id = ?
它不会主动再发一条 DELETE FROM sys_role_menus ...
TypeORM 的 delete 只管主表,不会像 update 里改 role.menus 那样去维护多对多关系。
中间表 sys_role_menus 会不会被清空?
会。 项目建表时给 role_id 配了 ON DELETE CASCADE:
sql
CREATE TABLE `sys_role_menus` (
`role_id` int NOT NULL,
`menu_id` int NOT NULL,
...
CONSTRAINT `FK_35ce749b04d57e226d059e0f633` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
删除 sys_role 某行时,MySQL 会自动删除 sys_role_menus 里所有 role_id = 该 id 的记录。
注意:
- 删的是关联记录,不是
sys_menu里的菜单本身。 - 若数据库没有这条 CASCADE(例如手工改过表结构),中间表可能残留脏数据。