TypeORM 知多少?聊聊业务实体中常见的多对多关系

749 阅读8分钟

前言

大家好!我是 嘟老板。在服务端项目中,各个业务实体基本不是孤立存在,彼此之间存在多种关联关系,比如权限控制中涉及的角色实体和权限实体,属于多对多的关系,一个角色可以拥有多个权限,一个权限也可以分配给多个角色,算是体现实体关系的典型场景。那 TypeORM 框架是如何处理实体间 多对多关系 的呢?又该如何实现呢?请带着这些问题,跟我走进今天的内容。

阅读本文您将收获:

  1. 了解 TypeORM 实体类如何定义 多对多关系
  2. 了解 多对多关系 相关装饰器及用法。
  3. 了解实体数据处理方法,如查询、新增、删除等。

关系分类

  • 一对一(One-to-One OTO
  • 一对多(One-to-Many OTM/ 多对一(Many-to-One MTO
  • 多对多(Many-to-Many MTM

本文重点讨论 多对多 关系。

MTM 关系介绍

多对多关系 中,一个实体可以与多个其他实体相关联,反之亦然。例如权限控制系统中的角色(Role)实体和权限(Auth)实体,一个角色可以拥有多个权限,一个权限也可以分配给多个角色。

多对多关系 在数据库层面的实现涉及以下三个部分:

  1. 实体表:每个实体类型(如角色和权限)都有其自己的表(如角色表和权限表),这些表存储了各自实体的数据。

  2. 关系中间表:为了表示实体间的多对多关系,需要一个额外的表,通常称为关系中间表或连接表。这个表不存储实体的具体数据,而是存储实体间关系的标识符,通常是两个实体表的主键(id)。

  3. 外键:在关系中间表中,每个实体的主键都会以外键的形式存在,这样就能够明确地标示出哪些实体是相关联的。

假设目前有两个表,角色表和权限表,分别存储了角色和权限的数据。为表示一个角色可以拥有多个权限,且一个权限可以分配给多个角色,就需要一个关系中间表,暂命名为 role-auth-relation。中间表至少包含以下两列:

  • roleId:存储角色 ID(作为外键指向角色表)
  • authId:存储权限 ID(作为外键指向权限表)

这样,通过该中间表就可以查询出哪些角色拥有哪些权限。

image.png

相关装饰器

@ManyToMany

多对多关系 必需,命名比较直接,应该都懂。

基本用法如下:

  1. 定义关联字段:例如角色类 Role 实例类中定义 authorizations 字段,用来存储角色关联的权限列表。
  2. 装饰器声明@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:可选,设置 主实体 的外键列,如 Roleid 列,以及中间表存储该列数据的列名称(roleId),作用同 @JoinColumn 装饰器。
  • inverseJoinColumn:可选,设置 从实体 的外键列,如 Authid 列,以及中间表存储该列数据的列名称(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 列。

数据操作

以下数据操作均已 RoleAuth 实体为例。

查询

可通过以下几种方式查询关联数据:

  1. 调用查询类 api,如 Repository.find,在 FindOptions 参数中指定 relations 配置项。
const roleRepository = dataSource.getRepository(Role)
const roles = await roleRepository.find({
    relations: {
        authorizations: true,
    },
})
  1. @ManyToMany 装饰器,options 参数设置 eagertrue,自动加载关联数据。

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()
  1. 使用 QueryBuilder 查询关联数据。

其中借助 leftJoinAndSelect api 添加左连接查询,以获取角色关联的权限数据。

const roles = await dataSource
    .getRepository(Role)
    .createQueryBuilder('role')
    .leftJoinAndSelect('role.authorizations', 'auth')
    .getMany()

eager 配置对于 QueryBuilder 方式无效,即便设置 eagertrue,也不会自动加载关联实体数据,必须调用 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 来单独保存 auth1auth2,通过启用 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 获取 id1 的角色(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 配置,会自动软删除关联的权限数据 auth1auth2

结语

本文重点介绍了 TypeORM 对于业务实体多对多关系的支持及相关用法介绍,包括装饰器(@ManyToMany@JoinTable)、数据操作(保存、删除、软删除)等,旨在帮助同学们加深对于业务实体多对多关系的应用理解。希望对您有所帮助。ZimuAdmin 服务端有对 TypeORM 的深度应用,欢迎 star

如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。

技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。


往期推荐