前言
大家好!我是 嘟老板。在服务端项目中,各个业务实体基本不是孤立存在,彼此之间存在多种关联关系,比如权限控制中涉及的角色实体和权限实体,属于多对多的关系,一个角色可以拥有多个权限,一个权限也可以分配给多个角色,算是体现实体关系的典型场景。那 TypeORM
框架是如何处理实体间 多对多关系 的呢?又该如何实现呢?请带着这些问题,跟我走进今天的内容。
阅读本文您将收获:
- 了解
TypeORM
实体类如何定义 多对多关系。- 了解 多对多关系 相关装饰器及用法。
- 了解实体数据处理方法,如查询、新增、删除等。
关系分类
- 一对一(
One-to-One OTO
) - 一对多(
One-to-Many OTM
)/ 多对一(Many-to-One MTO
) - 多对多(
Many-to-Many MTM
)
本文重点讨论 多对多 关系。
MTM 关系介绍
多对多关系 中,一个实体可以与多个其他实体相关联,反之亦然。例如权限控制系统中的角色(Role
)实体和权限(Auth
)实体,一个角色可以拥有多个权限,一个权限也可以分配给多个角色。
多对多关系 在数据库层面的实现涉及以下三个部分:
-
实体表:每个实体类型(如角色和权限)都有其自己的表(如角色表和权限表),这些表存储了各自实体的数据。
-
关系中间表:为了表示实体间的多对多关系,需要一个额外的表,通常称为关系中间表或连接表。这个表不存储实体的具体数据,而是存储实体间关系的标识符,通常是两个实体表的主键(
id
)。 -
外键:在关系中间表中,每个实体的主键都会以外键的形式存在,这样就能够明确地标示出哪些实体是相关联的。
假设目前有两个表,角色表和权限表,分别存储了角色和权限的数据。为表示一个角色可以拥有多个权限,且一个权限可以分配给多个角色,就需要一个关系中间表,暂命名为 role-auth-relation
。中间表至少包含以下两列:
roleId
:存储角色ID
(作为外键指向角色表)authId
:存储权限ID
(作为外键指向权限表)
这样,通过该中间表就可以查询出哪些角色拥有哪些权限。
相关装饰器
@ManyToMany
多对多关系 必需,命名比较直接,应该都懂。
基本用法如下:
- 定义关联字段:例如角色类
Role
实例类中定义authorizations
字段,用来存储角色关联的权限列表。 - 装饰器声明:
@ManyToMany
装饰器标注在实体类中的关联字段上。例如角色类Role
中的authorizations
字段。
import { Entity, PrimaryGeneratedColumn, PrimaryColumn, Column, ManyToMany, JoinTable } from 'typeorm'
import { Auth } from './auth.entity'
@Entity('role')
export class Role {
@PrimaryGeneratedColumn()
id!: number
// 其他属性 ...
@ManyToMany(() => Auth)
@JoinTable()
authorizations!: Auth[]
}
@ManyToMany
装饰器可接收以下参数:
typeFunctionOrTarget
:函数类型,返回与当前实体相关联的实体类的构造函数,例如() => Auth
表示authorizations
属性关联的是Auth
实体。inverseSide
:函数类型,双向关系 时可用,用于指定 外键列,要在两个实体类中都使用@ManyToMany
装饰器。
例如 Auth
实体类中定义 roles
字段,存储关联的角色数据列表。
import { Entity, PrimaryGeneratedColumn, PrimaryColumn, Column, ManyToMany } from 'typeorm'
import { Role } from './role.entity'
@Entity('auth')
export class Auth {
@PrimaryGeneratedColumn()
id!: number
// 其他属性...
@ManyToMany(() => Role, (role) => role.authorizations)
roles!: Role[]
}
同样的,Role
实体类中 authorizations
字段的 @ManyToMany
也需要指定第二个参数:
// ...
@ManyToMany(() => Auth, auth => auth.roles)
@JoinTable()
authorizations!: Auth[]
关联关系配置项,包含以下常用配置:
cascade
:若设置为true
,表示允许关联的实体数据同步 保存/更新/删除 到数据库表;也可设置为具体的操作数组,如['insert']
,表示进行指定操作时,才同步执行。nullable
:设置关联列的值是否 允许为空。lazy
:设置关联实体数据是否 延迟加载。eager
:设置是否 自动加载 关联实体数据。onDelete
:设置 删除 时的级联操作,比如删除主表数据时,关联表的数据是否一同删除。onUpdate
:设置 更新 时的级联操作,比如更新主表数据时,关联表的数据是否一同更新。- ...
// ...
@ManyToMany(() => Auth, auth => auth.roles, {
cascade: true, // 启用关联实体级联更新
eager: true // 自动加载关联实体数据
})
@JoinTable()
authorizations!: Auth[]
@JoinTable
专用于 多对多关系,上文有提到,多对多关系需要关系中间表来维护两个实体之间的关系,因此需要 @JoinTable
装饰器来指定中间表的配置。@JoinTable
装饰器可以指定中间表的名称、主实体 和 从实体 的 外键列 等信息。
多对多关系 涉及到两个实体,@JoinTable
只需要在一个实体的关联列上使用。其中使用 @JoinTable
装实体可称为 主实体,另外一个实体可称为 从实体。若两个实体定义为 双向关联 关系,其中 从实体 中关联 主实体 的操作称为 逆关联。
@JoinTable
装饰器可接收一个 对象参数,包括以下常用配置:
name
:可选,关系中间表表名,若不设置,则TypeORM
会根据两个关联的实体按照规则自动生成。joinColumn
:可选,设置 主实体 的外键列,如Role
的id
列,以及中间表存储该列数据的列名称(roleId
),作用同@JoinColumn
装饰器。inverseJoinColumn
:可选,设置 从实体 的外键列,如Auth
的id
列,以及中间表存储该列数据的列名称(authId
),作用同@JoinColumn
装饰器。- ...
如角色类 Role
关联字段 authorizations
:
import { Entity, PrimaryGeneratedColumn, PrimaryColumn, Column, ManyToMany, JoinTable } from 'typeorm'
import { Auth } from './auth.entity'
@Entity('role')
export class Role {
@PrimaryGeneratedColumn()
id!: number
// 其他属性 ...
@ManyToMany(() => Auth, (auth) => auth.roles)
@JoinTable({
name: 'zm-role-auth-relation',
joinColumn: {
name: 'role',
referencedColumnName: 'id'
},
inverseJoinColumn: {
name: 'auth',
referencedColumnName: 'id'
}
})
authorizations!: Auth[]
}
上述代码,指定中间表为 zm-role-auth-relation
;指定 Role
表外键为 id 列,且存储在中间表 roleId
列;指定 Auth
表外键为 id
列,且存储在中间表 authId
列。
数据操作
以下数据操作均已 Role
和 Auth
实体为例。
查询
可通过以下几种方式查询关联数据:
- 调用查询类
api
,如Repository.find
,在FindOptions
参数中指定relations
配置项。
const roleRepository = dataSource.getRepository(Role)
const roles = await roleRepository.find({
relations: {
authorizations: true,
},
})
@ManyToMany
装饰器,options
参数设置eager
为true
,自动加载关联数据。
Role
实体类中 authorizations
列定义如下:
@Entity('role')
export class Role {
@PrimaryGeneratedColumn()
id!: number
// 其他属性 ...
@ManyToMany(() => Auth, (auth) => auth.roles, { eager: true })
@JoinTable()
authorizations!: Auth[]
}
启用 eager
后,查询时无需指定 relations
配置项,即可自动带出关联数据。
const roleRepository = dataSource.getRepository(Role)
const roles = await roleRepository.find()
- 使用
QueryBuilder
查询关联数据。
其中借助 leftJoinAndSelect
api 添加左连接查询,以获取角色关联的权限数据。
const roles = await dataSource
.getRepository(Role)
.createQueryBuilder('role')
.leftJoinAndSelect('role.authorizations', 'auth')
.getMany()
eager
配置对于 QueryBuilder
方式无效,即便设置 eager
为 true
,也不会自动加载关联实体数据,必须调用 leftJoinAndSelect
。
保存/删除/软删除...
要想实现数据级联操作,需要在实体类关联列启用 cascades
。
@ManyToMany(() => Auth, (auth) => auth.roles, { cascades: true })
@JoinTable()
authorizations!: Auth[]
以下操作均是在已启用 cascades
的前提下。
保存
保存操作包括实体数据 新建 和 更新,可借助 EntityManager
save
api 实现。
const manager = dataSource.manager
const auth1 = new Auth()
auth1.code = "1"
auth1.name = "管理员权限"
const auth2 = new Auth()
auth2.code = "2"
auth2.name = "主管权限"
const role = new Role()
role.code = "1"
role.name = "Admin"
role.authorizations = [auth1, auth2]
await manager.save(role)
我们不需要额外调用 save
来单独保存 auth1
、auth2
,通过启用 cascades
配置,他们会在保存 role
时自动保存。
删除
删除操作也是借助 EntityManager
save
api 实现,因为多对多关系的删除操作,并不是对实体数据进行删除,而是删除 中间表 数据。
const role = await dataSource.getRepository(Role).findOne({
relations: {
authorizations: true,
},
where: { id: 1 }
})
// 待删除的 权限 id
const authIdsToRemove = [1, 2, 3]
role.authorizations = role.authorizations.filter((auth) => {
return !authIdsToRemove.includes(auth.id)
})
await dataSource.manager.save(role)
以上代码调用 findOne
获取 id
为 1
的角色(role
) 及关联的权限数据(role.authorizations
)后,通过 filter
筛选出不需删除的权限数组,并更新 role.authorizations
,这样调用 sava
保存后,则会更新关联中间表,仅保留 role.authorizations
中保留的关联关系。
软删除
软删除操作借助 EntityManager
softRemove
api 实现。
/**
* role 关联两条权限数据:auth1、auth2
*/
const role = await dataSource.getRepository(Role).findOne({
relations: {
authorizations: true,
},
where: { id: 1 }
})
await dataSource.manager.softRemove(role)
以上代码中,调用 softRemove
删除 role
时,由于启用了 cascades
配置,会自动软删除关联的权限数据 auth1
、auth2
。
结语
本文重点介绍了 TypeORM
对于业务实体多对多关系的支持及相关用法介绍,包括装饰器(@ManyToMany
、@JoinTable
)、数据操作(保存、删除、软删除)等,旨在帮助同学们加深对于业务实体多对多关系的应用理解。希望对您有所帮助。ZimuAdmin 服务端有对 TypeORM
的深度应用,欢迎 star。
如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。
技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。