前言
大家好!我是 嘟老板。在服务端项目中,各个业务实体基本不是孤立存在,彼此之间存在多种关系,比如 用户 和 用户档案,每个用户仅能维护一个用户档案,而每个档案也专属于一个用户,算是比较典型的 一对一关系。那 TypeORM 框架是如何处理实体间 一对一关系 的呢?又该如何实现呢?请带着问题,随我走进今天的内容。
阅读本文您将收获:
- 了解
TypeORM实体类如何定义 一对一关系。- 了解相关装饰器及用法。
- 了解实体数据处理方法,如查询、新增、删除等。
关系种类
- 一对一(
One-to-One OTO) - 一对多(
One-to-Many OTM) / 多对一(Many-to-One MTO) - 多对多(
Many-to-Many MTM)
本文重点讨论 一对一 关系。
相关文章:
OTO 关系介绍
一对一关系 中,在两个存在关系的实体间,每个实体的实例仅能与另一个实体的 一个 实例相关联,反之亦然。
一对一关系 在数据库层面的实现涉及以下两部分:
- 实体表:每个实体类型都有其自己的表,用于存储各自实体的数据。例如,存储用户的用户表和存储用户档案的档案表。
- 关联字段 & 外键:通常把一个实体的主键用作另一个实体的外键。例如,用户表中的 档案ID 通常是档案表的 主键 ID。
以 用户 和 用户档案 实体为例,需要分别设计 用户表 和 档案表,其中用户表除基础字段外,可额外设计 档案 ID 字段,用以关联用户档案。
装饰器介绍
@OneToOne
作用
一对一关系 专用装饰器,用来定义两个实体间的 一对一关系。
例如,用户实体类 User:
import { Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm'
import { Profile } from './profile.entity'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
// 其他属性字段...
@OneToOne(() => Profile)
@JoinColumn()
profile: Profile;
}
其中 @OneToOne 装饰器作用于 profile 字段,参数 () => Profile 表明 User 实体与 Profile 实体存在一对一关联关系,且 profile 字段存储关联的 Profile 数据。
参数
基于不同的应用场景,@OneToOne 装饰器可接受以下参数:
-
typeFunctionOrTarget:函数类型,返回与当前实体相关联的实体类的构造函数,例如() => Profile表示profile属性关联的是Profile实体。 -
inverseSide:函数类型,双向关系 时使用,用于定义关系的实体关联属性,双向关系 时两个实体的关联列都要用@OneToOne装饰器。
如 User 和 Profile 双向关联,需要将 User 类的 profile 字段调整如下:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
// 其他属性字段...
@OneToOne(() => Profile, (profile) => profile.user)
@JoinColumn()
profile: Profile;
}
其中 @OneToOne 第二个参数 (profile) => profile.user 表示 Profile 实体的 user 属性存储关联的 User 实体数据。
Profile 类需要定义 user 字段,并使用 @OneToOne 装饰器修饰。
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { User } from './user.entity'
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number
// 其他属性字段...
@OneToOne(() => User, (user) => user.profile)
user: User
}
-
options:关系配置项,包含以下常用配置:cascade:若设置为true,表示允许关联的实体数据同步 保存/更新/删除 到数据库表;也可设置为具体的操作数组,如['insert'],表示进行指定操作时,才同步执行。nullable:设置关联列的值是否 允许为空。lazy:设置关联实体数据是否 延迟加载。eager:设置是否 自动加载 关联实体数据。onDelete:设置 删除 时的级联操作,比如删除用户表数据时,用户档案表是否一同删除。onUpdate:设置 更新 时的级联操作,比如更新用户表数据时,用户档案表是否一同更新。- ...
@OneToOne(() => Profile, profile => profile.user, {
cascade: true,
eager: true,
onDelete: "CASCADE"
})
@JoinColumn()
profile: Profile;
以上代码表示启用级联查询,自动加载,且在删除用户数据时,同步删除关联的档案数据。
@JoinColumn
用途
该装饰器主要作用有以下两点:
- 定义两个实体间的关联列,即 外键。
- 允许我们自定义 外键列表字段名称 及其引用的另一个实体的 表字段。
@JoinColumn 仅在包含的外键的实例类中使用,比如 User 类包含外键列 profile,则只需加在 profile 属性上即可,不需要在 Profile 类中使用。
参数
@JoinColumn 装饰器可接收如下三个参数:
name:外键列的表字段名称。referencedColumnName:外键列指向的实体表字段名称。foreignKeyConstraintName:外键约束名称。
使用方式如下:
@OneToOne(() => Profile, profile => profile.user)
@JoinColumn({ name: 'profile_id', referencedColumnName: 'id' })
profile: Profile;
@JoinColumn 指明用户表 user 的外键列是 profile_id,指向档案表 profile 的 id 列。
数据操作
保存
保存操作包括数据 新增 和 更新,可借助 EntityManager save api 实现
若启用了 级联配置,即 @OneToOne 装饰器中设置 cascades 为 true,则只需调用一次 save 即可。
例如 User 类 profile 属性定义如下:
@OneToOne(() => Profile, profile => profile.user, {
cascade: true
})
@JoinColumn()
profile: Profile;
通过以下方式即可同时保存 User 和 Profile 数据。
const profile = new Profile()
profile.gender = "M"
profile.tel = "13912341234"
const user = new User()
user.name = "dulaoban"
user.profile = profile
await dataSource.manager.save(user)
查询
查询类 api
以 Repository.find 为例,参数中指定 relations 选项,设置 profile 为 true,表示要查询关联实体数据 profile。
const users = await dataSource.getRepository(User).find({
relations: {
profile: true,
},
})
自动加载 - eager
上文提到 @OneToOne 装饰器可接收三个参数,其中第三个参数是个配置对象,eager 就是配置项之一,表示自动加载关联的实体数据。
我们将 User profile 属性定义如下:
@OneToOne(() => Profile, profile => profile.user, {
eager: true
})
@JoinColumn()
profile: Profile;
然后调用查询类 api,如 Repository.find,无需指定 relations 配置项,即可自动查询关联实体数据。
const users = await dataSource.getRepository(User).find()
QueryBuilder
借助 leftJoinAndSelect api 添加并执行左连接查询,获取关联实体数据。
const users = await dataSource
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.getMany()
@OneToOne 的 eager 配置对于 QueryBuilder 方式无效,因此即便设置 eager 为 true,也不会自动加载关联实体数据,必须调用 leftJoinAndSelect。
多说两句
简单介绍下 leftJoinAndSelect api,后续会单独写篇 QueryBuilder 的文章:
从上面的例子可以看出,leftJoinAndSelect 接收了两个参数:
- 第一个参数表示要加载的关系,比如
user关联的profile。 - 第二个参数指定关系表的别名,用以简化
SQL查询中的引用,如指定档案表别名为profile,这个别名可在后续的任何位置引用,如指定查询条件where。
再次搬出查询 User 的例子:
const users = await dataSource
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.getMany()
以上代码对应的 SQL 语句如下:
SELECT user.*, profile.* FROM user user
LEFT JOIN profile profile ON user.profile_id = profile.id
结语
本文重点介绍了 TypeORM 对于业务实体一对一关系的支持及相关用法介绍,包括装饰器(@OneToOne、@JoinColumn)、数据操作(保存、查询)等,旨在帮助同学们加深对于业务实体一对一关系的应用理解。希望对您有所帮助。ZimuAdmin 服务端将有对 TypeORM 的深度应用,欢迎 star。
如您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。
技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。