添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

mysql 也算是比较经典的免费的关系型数据库了,很多人都在用,且很好用, MongoDB 是一个介于关系型和非关系型数据库之间,性能表现不错

由于主要学习数据库,我们就从最经典的 mysql 开始,使用 typeorm 映射的 mysql

mysql MongoDB

SQL基础教程 (如果数据库基础不是很好,可以参考这里,我也是参考这里回忆起来的数据库操作)

ps :数据库是不区分大小写的,因此我们设置字段时无需过分关注大小写,我们只要保持和数据库字段一致即可,另外传参时为了避免给客户端带来大小写的麻烦,最好采用 蛇形变量(_)

数据库工具仍然使用的是 Database Client

数据表 Entity

编写之前,注意引入 typeorm mysql ,前面有介绍

yarn add typeorm @nestjs/typeorm mysql2

是我们用于映射到数据库字段的入口,可以理解为,声明模型等于建立数据库表格

下面定义一个基础用户表格,需要注意的是我们自定义的字段,一定要设置默认值 default,否则可能会报错

ps:存放小数时很多老手都是推荐存放字符串,否则不同平台不同编译环境,会出现意想不到的精度问题,虽然了解后在意料之中,避免小数直接用数字是最好的,如果有些需要用到小数,后面改没办法,可以以将该参数转化为字符串返回即可(用的地方多了会稍微麻烦点,但也确实是一个好方案)

@Entity()  //默认带的 entity
//@Entity('user')  //可以设置数据库表格名称
export class User {
    //作为主键且创建时自动生成,默认自增
    @PrimaryGeneratedColumn()
    id: number
    //用户名,unique 唯一值, default 默认值
    @Column({ unique: true, default: null })
    account: string
    //默认数据库的列,会根据 ts 类型,自动创建自定类型,默认字符串 255 byte,也就是255个unicode字符
    @Column({default: null})
    nickname: string
    //可以设置唯一值,参数可以点进去看详情
    @Column({ unique: true, default: null })
    wxId: string
    //设置默认值
    @Column({ default: null })
    age: number
    //设置枚举,实际推荐数字 + 文档即可,方便又实惠,这里仅仅看着方便
    //枚举到数据库中实际上是字符串,因此如果设置成数字查询要额外注意了
    // @Column('simple-enum', { enum: ['0', 'woman', 'unknow'], default: 'unknow' })
    // sex: string
    @Column('simple-enum', { enum: [1, 2, 0], default: 0 })
    sex: number
    @Column({ default: null }) //默认最大字符串255字节,能储存255个unicode字符
    mobile: string
    //默认都是可变字节,如果设置最大长度比较小,但内容比较大,也能写入,但是效率可能会变低
    //默认最大字节数比较大,65535为text,另一个更大,也可以根据自行设置大小
    // @Column('mediumtext', {default: null})
    @Column('text', { default: null })
    desc: string
    //下面是创建内容自动生成,和更新时自动更新的时间戳,分别代表该条记录创建时间和上次更新时间
    @CreateDateColumn({ type: 'timestamp' })
    createTime: Date
    //更新内容时会自动更新时间戳
    @UpdateDateColumn({ type: 'timestamp' })
    updateTime: Date

数据表关系

数据表中有下面几种关系一对一、一对多、多对一、多对多,并且他们的关系往往是双向存在的(除非单纯的包含关系,那样可以是单向,无法反向查询)

OneToOne、OneToMany、ManyToOne、ManyToMany

上面会发现我们 user表 没有密码等信息,为了演示一对一,这里额外建立了一个 author表 用它来隐藏我们的隐私信息,也方便管理了,查询时也无需隐藏了,方便了很多

可能有人说一对一影响查询速度,没有存在的必要,我想说,我们选择使用关系型数据库,不只是为了性能,更多是为了安全,为了处理繁杂的关系而存在,我们的很多表格都是根据社会分工等关系建立的,否则很多东西全混杂到一个表格那才叫难受

例如:用户表 -- 在职企业信息(假设一个用户只有一个企业) -- 学生信息 等,要不要建立一对一呢,还是揉到一起呢

下面我们用上面的用户表,将 userauth 表关联起来,

user 表 设置 OneToOne 关联

//user表 添加 auth,并设置 一对一关系
@Entity()  //默认带的 entity
export class User {
    //用户隐私信息表,Auth类型,其中author 的 user 属性对应的自己
    @OneToOne(() => Auth, auth => auth.user)
    auth: Auth

给我们的 auth 表 设置 OneToOne 关联,使用@JoinColumn()设置外键,如果想自定义外键名称可以传递参数

//auth 表,添加用户信息
@Entity()  //默认带的 entity
export class Auth {
    //作为主键且创建时自动生成,默认自增
    @PrimaryGeneratedColumn()
    id: number
    @Column({ length: 30, default: null }) //可以设置长度30个字节
    password: string
    //身份证
    @Column({ length: 20, default: null})
    idCard: string
    //设置 User 类型,user 的 auth 属性指向自己
    @OneToOne(() => User, (user) => user.auth)
    @JoinColumn() //添加外键,建立表关联,会自动生成userId,这个id就是外键为另一个user表的primaryid
    user: User
    //这样给外键赋值时无需传递对象了,并且获取时还可以额外获得该属性,不需内容时无需连表查询
    //增加该属性并不会额外减少或者增加数据库字段,建立关系后数据库会自然的生成该字段,只不过我们没有映射
    @Column({ default: null })
    userId: number

需要注意的是,一定要在一个表格中设置 @JoinColumn(),其意思是设置外键关联,会给设置的表格自动添加一个外键id,并指向另外一个表格的 id 属性,

本案例给 auth 添加的外键,在数据库中,默认名字为 userId, 即,默认外键名称为: 参数 + Id,并且,我们赋值时,需要给设置外键的auth表的关联属性user赋值,这样就会自动取另一个对象userid属性会赋值给我们的外键 userId保存到数据库中

//给谁添加的外键(JoinColumn),谁需要被赋值,这个靠映射关联,因此保存之前确定映射关系也没问题
auth.user = user;
await this.authRepository.save(auth);

为什么给 author 添加外键呢,我们user是一个重要的表格,又可以称为主表,auth作为一个子表扩展是比较合理的(ps:外键是用于和其他表建立关系扩展功能用的,因此又叫做子表、关联表)

上面会发现我们我们建立关联时,给外键表格 auth 额外增加了一个 userId 字段,前面说了,这个字段即使不声明数据库也会存在,我们设置他也是为了获取数据时,额外获取一个userId,某些时候只需要对比 id,则可以不用联表查询了,另外建立关联的逻辑 是 给外键赋予另一个表的 id,所以,我们也可以直接给该 id 直接赋值建立关系(一定要确认另一个表的 id 存在哈)

auth.userId = user.id;//给谁添加的外键(JoinColumn),谁需要被赋值,注意userid保存前id不存在
await this.authRepository.save(auth);

看看我们建立的数据库表格吧

一对多/多对一

这个也是我们常用的功能,我们就以掘金为例,一个用户可以编写多篇文章,每篇文章只会对应一个用户,这就是典型的一对多/多对一,他们两个实际上是一样的,只不过从哪个角度看,一个是多对一、另一个必然是一对多

就以掘金的文章和用户为例,建立 一对多/多对一 关系表

需要注意的是,一对多/多对一外键一定是建立在 多对一 的一方,因为数据多的那方只需要设置一个外键,即可关联另外一个表格,而另一个表格获取多个时,只需要将外键和自己匹配的全部获取即可

给文章 article表 添加 多对一,并添加外键

//文章article表
@Entity()
export class Article {
    @PrimaryGeneratedColumn()
    id: number
    @Column({ default: null })
    title: string
    @Column({ default: null })
    desc: string
    @Column('mediumtext', { default: null })
    content: string
    //文章状态,默认创建即编辑中、等待审核、审核中、成功、失败
    //平时可以数字枚举或者个别字符,以提升实际效率和空间,文档注释最重要,这里纯粹为了看着清晰
    @Column('simple-enum', { 
        enum: [ArticleStatus.editing, ArticleStatus.waiting, ArticleStatus.checking, ArticleStatus.success, ArticleStatus.failure], 
        default: ArticleStatus.editing,
    status: ArticleStatus
    //设置多对一的关系,顺道添加一个外键
    @ManyToOne(() => User, user => user.articles)
    @JoinColumn() //设置外键
    user: User
    @Column({ default: null })
    userId: number
    //额外设置一个软删除,当必要时可以恢复
    @Column({ default: false, select: false })
    isDelete: boolean

给 user 表添加一对多关联

//user表
@Entity()
export class User {
    //我发布的文章,一个用户多篇文章,一对多
    @OneToMany(() => Article, article => article.user)
    articles: Article[]

多对多就一种,我们还是以掘金的文章和表格为例,一篇文章可以被多个不同的人收藏、一个人可以收藏多篇文章,这就是典型的一对多

多对多没有那么多要求,两个都是多对多,我们的关联参数都只能是数组,无法设置外键,因此我们就要使用 @JoinTable()新建立一个关系表格了(就是单纯的 article、user的id建立关系表)

//user表
@Entity()
export class User {
    //收藏文章
    //一篇文章会被多个人收藏,一个人可以收藏多篇文章
    @ManyToMany(() => Article, article => article.collects)
    collects: Article[]

这里我就以文章为主(多对多不分主次表,但使用的位置不同生成的数据库默认名称不同哈),在里面使用 @JoinTable()建立关联表格

//文章article表
@Entity()
export class Article {
    //假设需要显示收藏数很频繁,但收藏操作很不频繁,可以额外维护一个收藏数count
    @Column({ default: 0 })
    collectCount: number
    //一篇文章会被多个人收藏,一个人可以收藏多篇文章
    @ManyToMany(() => User, user => user.collects)
    @JoinTable() //多对多,会自动生成两个{nameId} + 主键的新表,表名:当前表名_当前键名_关联表名 例如:article_collects_user
    collects: User[]

这里在 articlecollects 上创建的关联 user 的关系表,因此系统给我们的关系表默认起的名字也很中规中矩,article_collects_user,参数分别为 articleIduserId

使用 @JoinTable()时, 我们一般无需自定义名称,毕竟本来就很清晰了(如果我们真的有强迫症,需要改名字改表格,外键也要驼峰式,与@JoinColumn()类似),如下所示

@JoinTable({
    name: 'article_collects_user', //表名,前后表名,后面键名,方便某时候直接快速查询关系表
    joinColumns: { name: 'article_id' }, //本表外键,默认是驼峰式
    inverseJoinColumns: { name: 'user_id' }, //另一个表的外键

看看我们新建立的关系表吧

ps:JoinTable虽然是最后出现,但一定是多对多才使用的么,可以思考一下

常用增删改查

我们的增删改查都是用的 typeorm,因此要参考他的文档进行学习和使用,具体的我们可以在使用时点进方法,相信能看到我们想要的

常用的增删改查,我们就介绍 nestjs 给的 Repository 方法直接进行操作

本篇后面也会介绍,带有部分数据库查询语句的 QueryBuilder, 对复杂操作对部分基础查询和查询进行优化

官网中文网

service编写前的配置

前面讲过,我们的逻辑基本上都是在 service 中编写的,因此 service 中也要进行基本配置,需要哪个模块引入那个模块即可

//service
@Injectable()
export class AuthService




    
 {
    constructor(
        //使用我们的的 userRepository 仓库,按照下面方式改成自己的类即可,需要哪个引用哪个即可
        @InjectRepository(User)
        private userRepository: Repository<User>,
        @InjectRepository(Auth)
        private authRepository: Repository<Auth>,
        @InjectRepository(User)
        private featureRepository: Repository<Feature>,
        @InjectRepository(Article)
        private articleRepository: Repository<Article>,

另外我们使用时,需要引入其他模块的的数据库 EntityService(我的service一开始也没多引用,好像也没事哈,但是数据不引用就会出问题,最好都引用了)

//module
@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    TypeOrmModule.forFeature([Auth]),
    TypeOrmModule.forFeature([Article]),
  controllers: [UserController],
  providers: [
    UserService, 
    AuthService,
    ArticleService,

增加和改动我们都是直接使用的 saveinsert、更新也可以使用 updatesave 最方便实用

ps:需要注意的是,新增、保存的时候会自动调用查询,不然参数中的 id 和 保存的信息是获取不到的,同时也可能会引出查询的错误,定位错误的时候需要注意

let auth = new Auth(); auth.password = loginInfo.password; await this.authRepository.save(auth); await this.authRepository.insert(auth); //增加多条 await this.authRepository.save([auth]); await this.authRepository.insert([auth]);

更新和增加不太一样,一般需要先查询,后更改,方便返回数据,其中 update 是不会返回我们的新数据的,另外手动赋值最累,效果有时候最理想

因为save保存会自动额外调用查询,所以 update 单纯从效率上来说要高于 save一些,可以看情况使用

因此如果想直接更新,不需要返回内容,可以直接使用 update + 查询条件 + 内容 即可,还能提升效率

async updateUser(
        userInfo: UserUpdateDto,
        user: User,
    //如果再原有对象更新的话,需要同时查询其所有关联,因此更新建议用 update
    user = await this.userRepository.findOneBy({
        id: user.id
    if (!user) {
        // 可以抛出一个异常告诉没找到,一般直接返回
        // throw new HttpException('该用户不存在', 204)
        return ResponseData.fail('该用户不存在')
    // update更新(推荐),传递的参数,不传递的不影响,不返回那条新user
    // 需要注意的是,如果参数不正确,会报错,id不正确,不会报错,优点是不需要额外查询,不严谨时减少开销
    // 可以加上 try -- catch
    // await this.userRepository.update({
    //     id: userId, //查询条件
    // }, userInfo)
    // 下面两种需要注意的是存在关联时不建议下面的 save 模式,容易丢关系或者报错
    // 可以直接save,简单粗暴,需要注意新数据的筛选,避免覆盖不该覆盖的
    //  await this.userRepository.save({
    //     ...user,
    //     ...userInfo
    //  })
    // 手动赋值,最累,处理复杂情况时使用,可以配合save或者update使用
    if (userInfo.age) user.age = userInfo.age;
    if (userInfo.mobile) user.mobile = userInfo.mobile;
    if (userInfo.nickname) user.nickname = userInfo.nickname;
    if (userInfo.sex) user.sex = userInfo.sex;
    await this.userRepository.save(user)
    return user

假如需要统一格式,赋予空时,可能传递为空字符串或者 null,如果想统一数据库格式,无论是手动赋值,还是update,那么最好手动处理一下数据,再更新

delete 直接根据某个参数删除, remove 根据 entity 删除,一般直接用 delete

//直接通过某个字段真删除,需要谨慎,一般重要数据软删除的比较多
await this.userRepository.delete({
    account: loginInfo.account

查询(条件、排序、分页)

查询是用的最多的,也是最杂的,findOneByfindOnefindByfindfindAndCountByfindAndCount这些也是用的最多的

by 系列的都是根据 entity 字段直接查询的,因此不具备条件、关系查询

findOne系列查询一个,返回对象

find系列查询多个,返回数组

findAndCount系列查询多个的同时,统计总数量,结果返回二维数组,第一个参数为查询数组内容,第二个为统计总数量

by 系列查询

//by系列查询
let user = await this.userRepository.findOneBy({
    account: loginInfo.account,

非 by 系列条件关系查询,whererelationsorder(包含主次关系排序)

let user = await this.userRepository.findOne({
    //默认选项都为 null 或 undefined时,搜索条件为没条件
    //为了避免选填内容,可以加上 Equal(name),如果需要查询为 null 的,用 IsNull()即可
    where: {
        account: "admin", //设置条件相符
    relations: {
        auth: true, //设置了之后,会自动连接查询,我们设置的外键那整对象都会被赋值了
    order: {
        createTime: 'DESC', //我们按照时间降序排列,升序 ASC,大小写没区别都支持
    //如果需要主次排序,那么可以传递对象,更复杂的需要用到builder查询了
    // order: {
    //     //我们按照时间降序排列,次级更新时间升序排列
    //     createTime: {
    //         direction: 'DESC',
    //         nulls: 'FIRST'
    //     },
    //     updateTime: {
    //         direction: 'ASC',
    //         nulls: 'LAST'
    //     }
    // },
    //relations: ['auth'], //也可以通过这种方式

分页查询 skiptake

let articles = await this.articleRepository.find({
    skip: 1, //自己计算页码和数量,这个是偏移量,第一页默认就是0,第二页默认就是 1 * 10
    take: 10, //页码 10

或查询,即:多个 where 查询条件取或

let user = await this.userRepository.findOne({
    where: [{
        account: 'admin', //同时查询用户名为 admin 或 年龄为 20 的用户
        age: 20

关联表条件查询(对关系表中的数据筛选),直接嵌套一层关联对象条件即可

let user = await this.userRepository.findOne({
    where: {
        account: "admin", //设置条件相符
        auth: {
            password: '123456', //对关联表进行查询
    relations: {
        auth: true, //设置了之后,会自动连接查询,我们设置的外键那整对象都会被赋值了

这里只介绍一部分,如果缺少,可以参考数据库的一些,另外可以使用一个,点进去看看支持哪些即可

Equal:是否相等,我们默认的筛选虽然也可以,但是无法查询为 null 的参数(如果参数为 null 那么默认的对比,会认为没有该筛选条件,即:如果全为 null,则返回数据库第一条数据或者全部数据),因此要上 EqualEqual(null)相当于 IS NULL,数据库没有 =NULL操作

let account = ...
let user = await this.userRepository.findOne({
    where: {
        account: Equal(account) //下面的也都是用到这里

NotNot("admin") !=

  • 注意:Not 不是 == 的对立关系,== 对立关系为 != 和 IS NULL,即 == 对立关系为 Not(...) 和 IsNull()
  • LessThanLessThan(10) <

    LessThanOrEqualLessThan(10) >=

    MoreThanLessThan(10) >

    MoreThanOrEqualLessThan(10) >=

    LikeLike("%mm_dd%")

  • 模糊查询 LIKE,区分大小写,%为匹配多个,_匹配一个
  • ILikeILike("%mm_dd%")

  • 模糊查询 ILIKE ,不区分大小写,%为匹配多个,_匹配一个
  • BetweenBetween(1, 10)

  • 介于 1~10 之间 BETWEEN...AND [1, 10],后面一般是开区间,mysql是两端闭区间
  • InIn([1, 2, 3])

  • 为包含关系 IN,包含一个符合条件
  • IsNullIsNull()

  • 相当于Equal(null),查询为空时的操作 IS NULL(数据库中没有=NULL的操作)
  • RawRaw("age+10=maxAge")

  • 使用数据库语句作为判断,但参数不能使用用户输入,避免注入攻击
  • 注意:很多数据库不区分大小写(mysql也是),且 mysql 中 LIKE、ILIKE 没有区别的,因此 LIKE 和 ILike 查出来的结果一样都是不区分大小写,且可能因为 ILIKE 关键字报错

    count:统计有多少条,支持分页

    countBy:和count一样但直接查询,不支持排序等

    sum:单列求和

    average:单列求平均值

    minimum:单列求最小值

    maximum:单列求最大值

    没有看到分组等功能,这个只能自己统计了

    QueryBuilder 增删改查

    中文网官网

    如果是日常的查询等功能,上面的就足够用了,对于一些比较比较复杂的查询,通过 QueryBuilder 可以简化代码,或者减少查询操作提高效率

    用的不多,可代替性很强,下面演示一下批量插入

    await this.userRepository.
        .createQueryBuilder()  
        .insert()  
        .into(User)  
        .values([{ account: "admin" }, { account: "admin1" }])  
        .execute();
    

    直接更新某条数据,可代替性很强

    await this.userRepository.
        .createQueryBuilder()  
        .update(User)  
        .set({ account: "admin" })  
        .where("id = :id", { id: 1 })  
        .execute();
    

    需要注意的是 from 和 where,别一不小心把全部的都删了哈

    await this.userRepository. 
        .createQueryBuilder()  
        .delete()  
        .from(User)
        .where("id = :id", { id: 1 })  
        .execute();
    

    查询(重点)

    先直接上一个普通的 where、relations 查询,也就是 where、leftjoin,我们的 where 语句可以使用 AND 也可以直接 andWhere(或就是 OR、orWhere)

    简单关联查询案例对比

    先上一个默认的关联查询

    await this.articleRepository.findOne({
        where: {
          id: body.id,
          isDelete: false,
        relations: {
          user: true,
    

    翻一下尝试一下,发现上面的更加清晰一些

    let article = await this.articleRepository
      .createQueryBuilder('article')
       //连接对象映射,第一个为映射到赋值的属性名,第二个为其重命名,方便条件查询
      .leftJoinAndSelect('article.user', 'user')
      .where('article.id=:id AND article.isDelete=:isDelete', {
        id: body.id,
        isDelete: false,
      .getOne()
      //也可以这样直接key-value的形式,要是或就是数组了.where([{id: body.id}])
      //需要注意涉及到没有数据 is null 的跟上面一样,直接字符串就行了,案例就不这么写了
      //这个条件为 null 时不会省略该语句(也不需要Equal(body.id))
    
    
    
    
        
    
      .where({
        id: body.id,
        isDelete: false,
    

    中等关联条件查询案例对比

    我们有这么一个需求,做一个收藏功能,需要判断某个用户是否收藏了某篇文章,再多一个表连接和查询条件,复杂了一点,发现也没问题,这次嵌套了关联查询条件

    let article = await this.articleRepository.findOne({
        where: {
          id: body.id,
          isDelete: false,
          collects: {
            id: user.id
        relations: {
          collects: true,
          user: true,
    if (!article) {
      return '该文章不存在'
    //查看是否收藏(这样做是否感觉性能稍低?)
    let isCollect = false
    if (article.collects?.length > 0) {
      isCollect = true
    article.collects = undefined;//不给用户返回这个
    //可以查一下文章是否被收藏
    return {
      ...article,
      isCollect
    

    完美翻译,发现还是上面的更清晰,我们多添加了一列,发现还是很ok的,只是看着代码多了

    let article = await this.articleRepository
      .createQueryBuilder('article')
      .leftJoinAndSelect('article.user', 'user')
      .leftJoinAndSelect('article.collects', 'collect')
      // .innerJoinAndSelect('article.collects', 'user')//这个一旦找不到主体也不返回,不适合
      .where('article.id=:id AND article.isDelete=:isDelete', {
        id: body.id,
        isDelete: false,
      .andWhere('collect.id=:userId', {
        userId: user.id
      .getOne()
    if (!article) {
      return '该文章不存在'
    let isCollect = false
    if (article.collects.length > 0) {
      isCollect = true;
    article.collects = undefined;
    return {
      ...article,
      isCollect
    

    复杂关联且或混合查询案例对比

    在看看下面即有且又有或的例子,表现就不是那么优秀了,QueryBuilder灵活性获得了大满贯

    我们有这么一个需求,查询一篇文章,支持 id、status、内容三者联合查询取交集,但内容需要根据 title、desc、content模糊查询;搜用户昵称时,支持模糊查询,满足即可,与前面是或的关系

    我们使用常用的查询查询一下,发现逻辑很复杂

    let articles = await this.articleRepository.findAndCount({
      //中括号是或的关系
      where: [
          //大括号内,同级是且,单个或使用In,也可使用其他查询
          //判断是否相符,如果不存在,就会自动忽略该条件了,由于模糊查询非常优秀
          //如果非要插 null 的,那么需要使用 Equal() 包装了
          id: body.id,
          title: body.name && Like(`%${body.name}%`), //Ilike忽略大小写,like不忽略大小写
          status: body.status && In(body.status), //这里面是或的关系
          id: body.id,
          desc: body.name && Like(`%${body.name}%`), //Ilike忽略大小写,like不忽略大小写
          status: body.status && In(body.status), //这里面是或的关系
          id: body.id,
          content: body.name && Like(`%${body.name}%`), //Ilike忽略大小写,like不忽略大小写
          status: body.status && In(body.status), //这里面是或的关系
        { //查询用户和其他选项是或的关系
          user: {
            //忽略大小写,模糊查询,对于一些不支持ILike的,那么LIKE和ILIKE一样都是不区分大小写
            nickname: ILike(`%${body.nickname}%`),
      relations: {
        user: true
    

    使用 QueryBuilder 改进,发现代码简洁了很多(需要注意的是可选参数的条件,没有时不应当存在)

    let articles = await this.articleRepository
      .createQueryBuilder('article')
      .leftJoinAndSelect('article.user', 'user')
      .where('article.id=:id AND article.status=:status', {
        id: body.id,
        status: body.status,
      .andWhere('article.title=:name OR article.desc=:name OR article.content=:name', {
        name: body.name
      .orWhere('user.nickname LIKE :nickname', {
        nickname: `%${body.nickname}%`, //注意一些数据库不支持 ILIKE,谨慎使用
      .getManyAndCount();
    

    前面介绍了基础的排序只支持两级(first、last),实际上可能有三级甚至更多,这是就要用到 builder 来进行多级排序了,如下所示就可以实现多级排序了(但如果出现了按照指定类型优先级排序问题,为了避免假分页的问题,可以将类型按照实际排序优先级设计大小,这样就可以更方便实现了),数据库也有更加详细的条件等排序可以搜索查看,这里 写的挺不错

    let articles = await this.articleRepository
          .createQueryBuilder('article')
          .orderBy('article.status', 'DESC')
          .addOrderBy('article.updateTime', 'DESC')
          .addOrderBy('article.createTIme', 'ASC')
    

    中文排序 CONVERT

    有时候会按照中文排序,直接使用 order by 会发现,排序看起来毫无章法(可能是按照编码排序,因此和拼音没关系),因此需要用到专门的参数 CONVERT 来排序

    let articles = await this.articleRepository
          .createQueryBuilder('article')
          .orderBy('CONVERT(article.name USING gbk)', 'ASC')
    

    查询进阶(补充)

    上面有提到查询进阶的内容,那是 typeorm 默认给定的,且发现 GROUP BY 分组功能 和 HAVING 句尾连接 没有

    这两个在基础封装没有,但是 QueryBuilder 中却是有的,不信尝试一下

    GROUP BY: 分组统计,结合统计函数,对一列或者多列对结果集进行分组统计

  • SELECT 列名A,统计函数(列名B)
  • FROM 表
  • WHERE ..
  • GROUP BY 列名A
  • 假设 A 为 名字,B为去过的地址,统计函数为Count,则可以统计某个人去到的地址数量
  • HAVING:前面的查询是默认可以使用 where 的,但是对于使用分组统计后的结果,却没有筛选条件,此时可以使用 HAVING 来对统计后的结果进行追加筛选,HAVING

  • SELECT 列名A,统计函数(列名B)
  • FROM 表
  • WHERE ..
  • GROUP BY 列名A
  • HAVING 统计函数(列名B) 查询条件
  • 假设 A 为 名字,B为去过的地址,统计函数为Count,查询条件为 > 10, 则可以统计某个人去到的地址数量超过10个的
  • 关联表更新(重点)

    你以为上面就结束了么,关联表更新的操作才是重中之重,这个除了优化代码,还是能优化效率

    我们又有了一个简单的需求,文章需要支持收藏功能,根据传递的参数决定是否收藏

    //我们发现无法直接更新,我们无法直接查询关联表(建立映射直接报错)
    //并且还需要先查询文章的收藏者,我们需要判断自身的索引,然后添加删除根据这个来进行跟新
    //倒是可以无需判断索引增加,利用抛出错误来解决,可是删除呢?没办法
    let article = await this.articleRepository.findOne({
      where: {
        id: body.id,
        isDelete: false,
      relations: {
        collects: true,
    if (!article) {
      return ResponseData.fail('该文章不存在');
    //查询收藏者
    let userId = user.id
    let index = article.collects.findIndex(e => e.id == userId)
    let isCollect = index >= 0
    //既然都对比了,我们可以减少无效操作
    if (body.is_collect && !isCollect) {
      //尚未收藏、进行收藏
      article.collects.push(user);
      // article.collects.push({id: user.id} as User);//如果用的不是user,是userid的话,转化一下,关系实际只也用到了主键id
    } else if (!body.is_collect && isCollect) {
      //已收藏、取消收藏
      article.collects.splice(index, 1)
    await this.articleRepository.save(article)
    return ResponseData.ok();
    

    使用 QueryBuilder 改进,发现逻辑清晰了很多

    //我们创建builder,后面在动态天添加、删除
    let builder = this.articleRepository
      .createQueryBuilder()
      .relation(Article, 'collects')
      .of(article) //我们的文章实体和user多对多,我们给collects添加新的user关联
    if (body.is_collect == 1) {
      try {
        //直接收藏,添加已存在的会报错,errno: 1062
        await builder.add(user)
      } catch(err) {
        if (err.errno === 1062) {
          //重复,也就是已经收藏
          return 'ok'
        }else {
          //其他的错误还没碰到,做了就做完善点
          let msg = err.msg
          return err.msg
    }else {
      //删除不存在的不会报错
      await builder.remove(user)
    

    上面就是根据QueryBuilder 对关联表操作的优势,我们直接看看怎么用吧,其他的基本用不到了

    this.articleRepository
      .createQueryBuilder()
      .relation(Article, 'collects')
      .of(article) //我们的文章实体和user多对多,我们给collects添加新的user关联
      // 多对多,一对多的情况使用 add、remove
      // .add(user) //添加关联
      // .remove(user) //移除关联
      // 一对一、多对一的情况使用 set
      // .set(user) //只有一个,即添加、更新关联
      // .set(null) //删除该关联
      // .loadOne(); //需要加载的话,可以直接加载一个到多个关联对象
    

    时间区间查询

    前面提到过between在mysql的问题,如果是开区间的,可以直接使用 querybuilder + 运算符来判断,能避免时间判断问题

    let day = dayjs(datetime)
    console.log(day.toDate(), day.add(1, 'day').toDate())
    // let articles = await this.articleRepository.findAndCount({
    //   where: {
    //     updateTime: Between(day.toDate(), day.add(1, 'day').toDate())
    //   },
    //   order: {
    //     updateTime: 'DESC'
    //   }
    // })
    let articles = await this.articleRepository.createQueryBuilder('user')
      .where('updateTime>=:start AND updateTime<:end', {
        start: day.toDate(),
        end: day.add(1, 'day').toDate()
      .orderBy('user.updateTime', 'DESC')
      .getManyAndCount()
    

    数据库表操作

    对于线上的功能,我们一般不会直接开启同步功能,因此会使用一些常用的表操作,然后更新 entity 表,以减少损失的可能性,下面介绍一些常见的表操作

    CREATE TABLE user(
     id INT PRIMARY KEY AUTO_INCREMENT,
     name VARCHAR(100)
    

    删除表(谨慎操作)

    DROP TABLE aaaaaa;
    

    其中 ALTER TABLE 表示修改的表,后面是更改语句

    ALTER TABLE user
    ADD idcard VARCHAR(18);
    

    修改列类型

    ALTER TABLE user
    MODIFY name VARCHAR(200);
    

    ps:这个里面删除 uni 等约束直接改完走同步即可,不必使用操作语句,添加约束涉及到内容不满足的会失败,可以走迁移逻辑😂

    ALTER TABLE user
    RENAME COLUMN idcard to mobile;
    

    删除列(谨慎操作)

    ALTER TABLE user
    DROP COLUMN mobile;
    

    多人开发与数据表更改注意

    平时我们独立开发的时候,一般都是 mysqlsynchronize: true,并且 yarn start:dev,这样个人开发是很舒服的

    但团队开发时尽量不要这样,这个 synchronize 会直接将我们更改的 entity 更新同步到数据库,配合yarn start:dev的及时更新,对于多人开发来说可能就是一个灾难(主要针对于对列的增改)

  • 两个人同时连接一个远端数据库,一个人增加了entity 和数据库字段,另一个人还没来得及拉取,那么会出现没有拉取的人会把拉取的人的数据库字段更改同步没了(他那里并没有新增字段,因此可能会导致新增的一列数据全没了),甚至出现报错
  • 对于删除影响不大,最多报个错,多个字段
  • 而对于修改字段类型、修改名称,这一个简直是一个灾难片,系统无法进行精准判断,会直接删除该列,然后创建新的一列,这一操作会直接导致原有数据列直接清空,并且多人开发过程中,即使这边进行了正确操作,另外一边没有及时拉取,那边如果开启同步运行的话,会出现修改会原字段的情况
  • 那么我们该如何操作呢

    有人说直接使用命令通过数据库,将数据库字段映射成entity,不开启synchronize,但仍然会出现多人开发时出现的问题,会把自己新增或者修改的字段直接全部覆盖掉,因此仍然存在隐性问题,并且会降低开发速度,很不舒适

    那怎么解决最好呢?

    解决方案一

    个人感觉,多人开发过程中,synchronize 可以关闭(synchronize:false),然后每次想同步的时候,拉去一下最新代码(别人更新的代码),然后直接临时将 synchronize 开启(synchronize:true),然后yarn start运行,运行完之后会自动同步,同步后关了 synchronize,再重新运行即可

    解决方案二

    在我们的 src 中建立下面文件,就起名叫 typeorm-config.ts 吧,缺点是需要手动导入 entities

    export const config: TypeOrmModuleOptions = {
        type: 'mysql',
        host: ...,
        port: ...,
        username: ...,
        password: ...,
        database: ...,
        entities: [User, Auth], //这个需要手动导入,不然不会同步该数据库表格
        synchronize: false,
    //这个是给脚本用的,脚本会根据这个参数来更新一次
    export const AppDataSource: any = new DataSource(config as DataSourceOptions);
    

    然后增加脚本,每次想同步时,先拉去git代码更新,然后执行下面命令手动更新即可(yarn sync),需要注意的是 entity 引入外键的路径,要使用相对路径

    "scripts": {
        "sync": "ts-node ./node_modules/typeorm/cli -d ./src/typeorm-config.ts schema:sync"
    

    存在上线应用的操作

    当需要部署应用到线上时,并且我们的线上应用已经存在数据库的情况,我们需要对比使用命令手动操作数据库(一般对于修改列名的操作比较谨慎,因此记录一下修改哪里了,记录下来),即可,即:对于修改列名,只需要先手动修改数据库列名,使其和entity一致就行了,对于新增和删除的的这个不会有问题,直接手动 synchronize 就没问题了(方案一、二都没问题)

    此外,对于存在线上数据库,还有另外一个操作对于我们来说相对比较安全,基本上不存在上述的同步问题,那就是数据迁移,当我们进行后续开发时,更改元数据表时,直接新增一个 entity,名字稍微改的不一样,例如:user -> user1,后面可以理解为版本号,然后全改成使用 新 entity 就行了,这样对于线上的表格就不会有任何影响了,然后需要做的数据迁移也很简单,我们直接写一个数据迁移接口(脚本),手动取出原数据库中所有数据,然后映射保存到最新的数据表即可(内存不够大就分页迁移,假如一次迁移10w条),此时,如果还想用之前的用户表名,直接再反向操作一次即可

    最后,为了谨慎,部署前我们最好备份我们的数据库,避免出现其他意外情况,必要时可以恢复

    对此你还有什么更好的方案呢,欢迎讨论

    如果有一个功能, 一个用户可以上传多个图片信息,一个订单可以传递多个图片信息,一个表格同时多对一两个表格,这样的你怎样设计呢?(个人提示:关联表),你有什么更好的思路么?

    这篇文章东西还挺多的,但是如果有了点数据库基础,那么就真的不多了,甚至这篇文章可以收藏当做一个问题参考点,看了这些基本上复杂一些的操作也可以做了

    最后,祝大家学习愉快!