nestjs-关系数据库(mysql)模型、关系、操作(三)

前言

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

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

mysqlMongoDB

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

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

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

数据表 Entity

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

js 复制代码
yarn add typeorm @nestjs/typeorm mysql2

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

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

ps:存放小数时一定要存放字符串,否则会发生意想不到的精度问题,虽然了解后在意料之中,避免小数直接用数字是最好的

js 复制代码
@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 关联

js 复制代码
//user表 添加 auth,并设置 一对一关系

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

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

js 复制代码
//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保存到数据库中

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

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

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

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

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

一对多/多对一

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

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

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

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

js 复制代码
//文章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 表添加一对多关联

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

多对多

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

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

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

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

js 复制代码
//文章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()类似),如下所示

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

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

常用增删改查

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

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

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

官网中文网

service编写前的配置

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

js 复制代码
//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一开始也没多引用,好像也没事哈,但是数据不引用就会出问题,最好都引用了)

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

增改

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

js 复制代码
//增加
let auth = new Auth();
auth.password = loginInfo.password;
...
await this.authRepository.save(auth);

//增加多条
await this.authRepository.save([auth]);

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

如果想直接更新,不返回内容,可以直接使用 update + 查询条件 + 内容 即可

js 复制代码
async updateUser(
        userInfo: UserUpdateDto,
        user: User,
    ) {
    user = await this.userRepository.findOneBy({
        id: user.id
    })
    if (!user) {
        // 可以抛出一个异常告诉没找到,一般直接返回
        // throw new HttpException('该用户不存在', 204)
        return ResponseData.fail('该用户不存在')
    }
    // //update只更新,不返回那条新user,支持设置查询条件,需要额外处理查询不到的数据
    // await this.userRepository.update({
    //     id: userId, //查询条件
    // }, userInfo)

    // //也可以直接save,简单粗暴,可能会把不想改的也改了
    //  await this.userRepository.save({
    //     ...user,
    //     ...userInfo
    //  })

    // 手动赋值,最累,但是存在问题最少
    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
}

删除

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

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

查询

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

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

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

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

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

by 系列查询

js 复制代码
//by系列查询
let user = await this.userRepository.findOneBy({
    account: loginInfo.account,
});

非 by 系列条件关系查询,whererelationsorder

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

分页查询 skiptake

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

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

js 复制代码
let user = await this.userRepository.findOne({
    where: [{
        account: 'admin', //同时查询用户名为 admin 或 年龄为 20 的用户
    },{
        age: 20
    }]
});

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

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

进阶查询

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

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

js 复制代码
let account = ...
let user = await this.userRepository.findOne({
    where: {
        account: Equal(account) //下面的也都是用到这里
    }
});

NotNot("admin") !=

LessThanLessThan(10) <

LessThanOrEqualLessThan(10) >=

MoreThanLessThan(10) >

MoreThanOrEqualLessThan(10) >=

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

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

BetweenBetween(1, 10),介于 1~10 之间 [1, 10),后面一般是开区间,这里是开区间

InIn([1, 2, 3])为包含关系,包含一个符合条件

IsNullIsNull() 查询为空的

RawRaw("age+10=maxAge"),使用数据库语句作为判断,但参数不能使用用户输入,避免注入攻击

统计

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

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

sum:单列求和

average:单列求平均值

minimum:单列求最小值

maximum:单列求最大值

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

QueryBuilder 增删改查

中文网官网

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

插入

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

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

更新

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

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

删除

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

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

查询(重点)

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

简单关联查询案例对比

先上一个默认的关联查询

js 复制代码
await this.articleRepository.findOne({
    where: {
      id: body.id,
      isDelete: false,
    },
    relations: {
      user: true,
    },
})

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

js 复制代码
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()

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

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

js 复制代码
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的,只是看着代码多了

js 复制代码
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模糊查询;搜用户昵称时,支持模糊查询,满足即可,与前面是或的关系

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

js 复制代码
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: {
        nickname: ILike(`%${body.nickname}%`), //忽略大小写,模糊查询
      }
    }
  ],
  relations: {
    user: true
  },
});

使用 QueryBuilder 改进,发现代码简洁了很多

js 复制代码
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}%`,
  })
  .getManyAndCount();

关联表更新(重点)

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

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

js 复制代码
//我们发现无法直接更新,我们无法直接查询关联表(建立映射直接报错)
//并且还需要先查询文章的收藏者,我们需要判断自身的索引,然后添加删除根据这个来进行跟新
//倒是可以无需判断索引增加,利用抛出错误来解决,可是删除呢?没办法
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 改进,发现逻辑清晰了很多

js 复制代码
//我们创建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 对关联表操作的优势,我们直接看看怎么用吧,其他的基本用不到了

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

数据库表操作

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

创建表

js 复制代码
CREATE TABLE user(
 id INT PRIMARY KEY AUTO_INCREMENT,
 name VARCHAR(100)
);

删除表(谨慎操作)

js 复制代码
DROP TABLE aaaaaa;

添加列

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

js 复制代码
ALTER TABLE user
ADD idcard VARCHAR(18);

修改列类型

js 复制代码
ALTER TABLE user
MODIFY name VARCHAR(200);

更改列名

js 复制代码
ALTER TABLE user
RENAME COLUMN idcard to mobile;

删除列(谨慎操作)

js 复制代码
ALTER TABLE user
DROP COLUMN mobile;

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

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

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

例如:

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

那么我们该如何操作呢

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

那怎么解决最好呢?

个人感觉,多人开发过程中,synchronize 可以开启,但一定不要使用 yarn start:dev,要使用yarn start,避免更新过于及时,此外还一定要及时沟通,当增加或者修改列时,及时告诉其他人,立即拉取更新,避免出现一系列同步问题,对于更新列出现的该列数据消失问题,我们可以先通过上一小节的数据库表操作直接MODIFY、RENAME直接修改,不会出现数据丢失问题,然后再手动更改我们的表即可,或者同步过去

存在上线应用的操作

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

因此,开发阶段,我们对于新增、修改列时,一定要及时通知队友拉取更新同步代码

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

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

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

最后

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

最后,祝大家学习愉快!

相关推荐
kongxx10 天前
NestJS中使用Guard实现路由保护
nestjs
白雾茫茫丶11 天前
Nest.js 实战 (十二):优雅地使用事件发布/订阅模块 Event Emitter
nestjs·nest.js·发布订阅·event emitter
lph65821 个月前
比起上传资源更应该懂得如何资源回收
node.js·nestjs
gsls2008081 个月前
将nestjs项目迁移到阿里云函数
阿里云·云计算·nestjs·云函数
d3126975101 个月前
在Nestjs使用mysql和typeorm
mysql·express·nestjs·typeorm
剪刀石头布啊2 个月前
nestjs-版本控制
nestjs
潇洒哥gg2 个月前
重生之我在NestJS中使用jwt鉴权
前端·javascript·nestjs
huangkaihao2 个月前
【NestJS学习笔记】 之 自定义装饰器
前端·node.js·nestjs
鹿鹿鹿鹿isNotDefined2 个月前
Nest 源码解析:依赖注入是怎么实现的?
nestjs
剪刀石头布啊2 个月前
nestjs-自定义装饰器
nestjs