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条),此时,如果还想用之前的用户表名,直接反向操作即可

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

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

最后

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

最后,祝大家学习愉快!

相关推荐
XiaoYu20026 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu20028 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄8 天前
NestJS 调试方案
后端·nestjs
当时只道寻常11 天前
NestJS 如何配置环境变量
nestjs
濮水大叔23 天前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi23 天前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi24 天前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs
Gogo8161 个月前
nestjs 的项目启动
nestjs
没头发的卓卓1 个月前
新手入门:nest基本使用规则(适合零基础小白)
nestjs