TypeORM 知多少? 一文搞懂业务实体中常见的一对一关系

481 阅读7分钟

前言

大家好!我是 嘟老板。在服务端项目中,各个业务实体基本不是孤立存在,彼此之间存在多种关系,比如 用户用户档案,每个用户仅能维护一个用户档案,而每个档案也专属于一个用户,算是比较典型的 一对一关系。那 TypeORM 框架是如何处理实体间 一对一关系 的呢?又该如何实现呢?请带着问题,随我走进今天的内容。

阅读本文您将收获:

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

关系种类

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

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

相关文章:

  1. 《TypeORM 知多少? 一文搞懂业务实体中常见的一对一关系 》
  2. 《TypeORM 知多少? 一文搞懂业务实体中常见的一对一关系 》(进行中)
  3. 《TypeORM 知多少?一文搞懂业务实体中常见的多对多关系》

OTO 关系介绍

一对一关系 中,在两个存在关系的实体间,每个实体的实例仅能与另一个实体的 一个 实例相关联,反之亦然。

一对一关系 在数据库层面的实现涉及以下两部分:

  • 实体表:每个实体类型都有其自己的表,用于存储各自实体的数据。例如,存储用户的用户表和存储用户档案的档案表。
  • 关联字段 & 外键:通常把一个实体的主键用作另一个实体的外键。例如,用户表中的 档案ID 通常是档案表的 主键 ID

用户用户档案 实体为例,需要分别设计 用户表档案表,其中用户表除基础字段外,可额外设计 档案 ID 字段,用以关联用户档案。

image.png

装饰器介绍

@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 装饰器。

UserProfile 双向关联,需要将 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,指向档案表 profileid 列。

数据操作

保存

保存操作包括数据 新增 和 更新,可借助 EntityManager save api 实现

若启用了 级联配置,即 @OneToOne 装饰器中设置 cascadestrue,则只需调用一次 save 即可。

例如 Userprofile 属性定义如下:

@OneToOne(() => Profile, profile => profile.user, {
    cascade: true
})
@JoinColumn()
profile: Profile;

通过以下方式即可同时保存 UserProfile 数据。

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()

@OneToOneeager 配置对于 QueryBuilder 方式无效,因此即便设置 eagertrue,也不会自动加载关联实体数据,必须调用 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

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

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


往期推荐