写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)

写给初用Nestj做项目的你(四篇: typeorm操作mysql数据库, 内附坑点罗列) TypeORM
???? 简单理解他就是一款帮助我们操作数据库的工具, nest.js对他做了很好的集成, 虽然它的官网写的挺全的但是实际开发起来还是不太够, 并且里面有大坑我会把我知道的都列出来, 这篇也会把一些常见的解决方案写出来。
1. 链接数据库
这次是针对mysql数据库

yarn add @nestjs/typeorm typeorm mysql2 -S

/share/src/app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmModule.forRoot({ port: 3306, type: 'mysql', username: 'root', host: 'localhost', charset: 'utf8mb4', password: '19910909', database: 'learn_nest', synchronize: true, autoLoadEntities: true, }),], // ...

  1. 上面演示的是链接我本地的mysql, database是库名。
  2. 可以在imports 里面定义多个 TypeOrmModule.forRoot 可以操作多个库, 多个时还需要填写不同的name属性。
  3. synchronize 自动载入的模型将同步。
  4. autoLoadModels 模型将自动载入。
当前的数据库:
写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

创建模块
// 控制台里输入创建命令 nest g module modules/goods nest g controller modules/goods nest g service modules/goods

【写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)】/share/src/modules/goods/goods.controller.ts
import { Controller, Get } from '@nestjs/common'; import { GoodsService } from './goods.service'; @Controller('goods') export class GoodsController { constructor( private readonly goodsService: GoodsService ) {}@Get() getList() { return this.goodsService.getList(); } }

建立实体 ???? 实体其实就是对应了一张表, 这个实体的class名字必须与表名对应, 新建entity文件夹 /share/src/modules/goods/entity/goods.entity.ts:
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Goods { @PrimaryGeneratedColumn() id: number; @Column() name: string; }

  1. @PrimaryGeneratedColumn()装饰了id为主键, 类型为数字。
  2. @Column()装饰普通行, 类型为字符串, 更多细节后面再讲。
引入实体 nest自身设计的还不是很好, 引入搞得好麻烦 /share/src/modules/goods/goods.module.ts:
import { Module } from '@nestjs/common'; import { GoodsController } from './goods.controller'; import { GoodsService } from './goods.service'; import { Goods } from './entity/goods.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [TypeOrmModule.forFeature([Goods])], controllers: [GoodsController], providers: [GoodsService] }) export class GoodsModule { }

  1. forFeature() 方法定义在当前范围中注册哪些存储库。
/share/src/modules/goods/goods.service.ts:
import { Injectable, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Goods } from './entity/goods.entity' import { Repository } from 'typeorm'; @Injectable() export class GoodsService { constructor( @InjectRepository(Goods) private goodsRepository: Repository ) { } getList() { return this.goodsRepository.find() } }

  1. @InjectRepository()装饰器将goodsRepository注入GoodsService中。
  2. 被注入进来的Repository都自带属性, 这里使用了自带的find方法后面会举例出更多。
写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

二. 坑点罗列(重点)
???? 满纸荒唐言, 一把辛酸泪, 当时我被坑的不浅。
1. 实体的强替换, 莫名删表 (坑人指数 ?? ?? ?? ??) ???? 以我们上面设置的实体为例:
export class Goods { @PrimaryGeneratedColumn() id: number; @Column() name: string; }

???? 我们初始化的表里面name字段对应的类型是varchar(45), 但是name: string; 这种方式初始化的类型是varchar(255), 此时类型是不一致的, typeorm选择清空我们的name列, 是的你没听错name列被清空了:
写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

???? 并且是只要你运行nest项目的时候就同步热更新了, 完全无感, 甚至你都不知道被清空了, 如果此时是线上环境请准备点干粮'跑路'吧。
???? 不光是string类型, 其他任何类型只要对不上就全给你删了, 毫无提示。
2. 没有集成现有数据库的方案 (坑人指数 ?? ?? ??) ???? 我们很多时候数据库都是已有数据的, 全新的空白数据库空白表的情况并不是主流, 在typeorm官网也并没有找到很好的接入数据库的方案, 全部都是冒着删库的危险在定义类型, 更有甚者你改到一半不小心自动保存了, 那么你的表就空了...
???? 我们不可能每次都是用空白数据库开发, 这点真难得很难人忍受。
3. entities的三种设置方式 (坑人指数 ??) 第一种: 单独定义
/share/src/app.module.ts配置链接数据库时:
TypeOrmModule.forRoot({ //... entities: [Goods, User], }),],

你用到哪些实体, 就逐一在此处引入, 缺点就是我们每写一个实体就要引入一次否则使用实体时会报错。
第二种:
自动加载我们的实体,每个通过forFeature()注册的实体都会自动添加到配置对象的entities数组中, forFeature()就是在某个service中的imports里面引入的, 这个是比较推荐的:
TypeOrmModule.forRoot({ //... autoLoadEntities: true, }),],

第三种:
自定义引入路径, 这个居然是官方推荐...
TypeOrmModule.forRoot({ //... entities: ['dist/**/*.entity{.ts,.js}'], }),],

4. entities的大坑点, 莫名引入 (坑人指数 ?? ?? ?? ?? ??) ???? 当我们使用上述第三种方式引入实体时, 一个超级bug出现了, 情景步骤如下:
  1. 我要写一个user的实体。
  2. 我直接复制了goods.entity.ts实体的文件改名为user.entity.ts
  3. 修改其内部的属性, 比如定义了userName, age, status等新属性, 删除了商品价格等旧属性。
  4. 但是我们还没有把导出的Goods类名改成User, 由于编辑器失去焦点等原因导致vscode自动保存了。
  5. 惊喜来了, 你的goods表被清空了, 是的你还没有在任何地方引用这个user.entity.ts文件, 但是它已经生效了, 并且无声无息的把你的goods表清空了。
  6. 我当时问该项目的负责人如何避免上述问题, 他研究了一下午, 告诉我关闭自动保存...(告辞)
5.官网的误导 (坑人指数 ?? ??) ???? 如此坑的配置方式, 竟然在官网里找到了3处推荐如此使用, 简直无语。
写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

6. 多人开发, 极其混乱 (坑人指数 ?? ?? ?? ?? ??) ???? 这个多人开发简直是噩梦, 互相删表的情况逐渐出现, 一个实际的例子比如a同事优化所有实体的配置比如统一把varchar(255)改成varchar(45), 所有的相关数据都会被清空, 于此同时你发现了问题, 并把数据补充回来了, 但此时b同事的电脑里还是varchar(255)版本, 一起开发时就会导致你不管怎么改数据, 表里的数据都会被反复清除干净...
???? 我们团队当时解决方案是, 每个人都复制一份当前库单独进行开发, 几个人开发就要有几个不同的库, 我们的mysql里全是已自己姓名命名的库。
???? 每次git拉取代码都要修改库名, 否则会把其他人的库清空;
7. 多版本开发 (坑人指数 ?? ?? ?? ?? ??) ???? 比如张三使用的是zhangsan_xxx库, 但是他同时开发几个版本, 这几个版本之前表的格式有差别, 那么张三要使用zhangsan_xxx_1_1, zhangsan_xxx_1_2这种命名格式来进行多个库的开发。
综上所述除非公司已经定了技术选型, 否则我不建议用nest开发... 三. entity设置
???? 看完坑点别灰心, 该学还得学, 下面我们介绍一下entity设置可以设置的比较实用的类型:
import { Entity, Column, Timestamp, UpdateDateColumn, CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm'; export enum GoodsStatus { NORMAL = 1, HOT = 2, OFFSHELF = 3, }@Entity() export class Goods { @PrimaryGeneratedColumn() id: number; @Column({ unique: true, nullable: false }) name: string; @Column({ length: 256, default: '暂无' }) remarks: string; @Column({ default: true }) isActive: boolean; @Column({ type: 'enum', enum: GoodsStatus, default: GoodsStatus.NORMAL, }) status: GoodsStatus; @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) putDate: Timestamp; @CreateDateColumn() createDate: Timestamp; @UpdateDateColumn() updateDate: Timestamp; }

  1. nullable: false 不可以为空。
  2. unique: true 唯一值, 不允许有重复的name的值出现, 需要注意的是如果当前的表里面已经有重复的nametypeorm会报错, 所以如果设置失败请检查表内容。
  3. length: 256限制字符的长度, 对应varchar(256)
  4. default: '暂无'默认值, 要注意当你手动设置为空字符串时并不会被设置为默认值。
  5. type: 'enum定义为枚举类型, enum: GoodsStatus 指定枚举值, 当你赋予其非枚举值时会报错。
  6. type: 'timestamp'定义类型为时间格式, CURRENT_TIMESTAMP默认就是创建时间。
  7. @CreateDateColumn()这个自动就可以为我们设置值为创建时间。
  8. @UpdateDateColumn()以后每次更新数据都会自动的更新这个时间值。
四. find方法类, 简洁的查找命令
???? 上面我们已经将goodsRepository注入到了GoodsService里面可以直接使用:
constructor( @InjectRepository(Goods) private goodsRepository: Repository ) { }

1. 无条件查询所有数据 this.goodsRepository.find()查询goods表的全部数据, 以及每条数据的信息。
2. 只显示name, createDate两列数据:
this.goodsRepository.find({ select: ['name', 'createDate'] })

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

3. 搜索名字是'x2'并且isActive为'false'的数据
this.goodsRepository.find({ where: { name: 'x2', isActive: false } })

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

4. 名字等于'x2'或者等于'x3'都会被匹配出来:
this.goodsRepository.find({ where: [{ name: 'x2', }, { name: 'x3' }] })

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

5. 排序, 以name降序, 创建时间升序排列
this.goodsRepository.find({ order: { name: "DESC", createDate: "ASC" } })

6. 切割, skip跳过1条, take取出3条
this.goodsRepository.find({ skip: 1, take: 3 })

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

7. like模糊查询名字里带有2的项, notid不是1
this.goodsRepository.find({ where: { id: Not(1), name: Like('%2%') } })

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

8. findAndCount 把满足条件的数据总数返回 数据是数组形式, [0]是匹配到的数组, [1]是符合条件的总数可能与[0]的长度不相同。
this.goodsRepository.findAndCount({ select: ['name'] });

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

9. findOne 只取配到的第一条, 并且返回形式为对象而非数组:
this.goodsRepository.findOne({ select: ['name'] });

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

10. findByIds, 传入id组成的数组进行匹配
this.goodsRepository.findByIds([1, 2]);

这个就不展示了。
11. 前端获取一个需要分页的列表 用户传入需要模糊匹配的name值, 以及当前第n页, 每页s条, 总数total条。
async getList(query) { const { keyWords, page, pageSize } = query; const [list, total] = await this.goodsRepository.findAndCount({ select: ['name', 'createDate'], where: { name: Like(`%${keyWords}%`) }, skip: (page - 1) * pageSize, take: pageSize }) return { list, total } }

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

五. dto 新增与修改
yarn add class-validator class-transformer -S

新增 先建立一个简单的新增dto模型/share/src/modules/goods/dto/create-goods.dto.ts:
import { IsNotEmpty, IsOptional, MaxLength } from 'class-validator'; export class CreateGoodsDto { @IsNotEmpty() name: string; @IsOptional() @MaxLength(256) remarks: string; }

使用/share/src/modules/goods/goods.service.ts
create(body) { const { name, remarks } = body; const goodsDto = new CreateGoodsDto(); goodsDto.name = name; goodsDto.remarks = remarks; return this.goodsRepository.save(goodsDto) }

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

更新 老样子, 先建立一份更新的dto, 比如name是不可以更新的就不写name, /share/src/modules/goods/dto/updata-goods.dto.ts:
import { MaxLength } from 'class-validator'; export class UpdataGoodsDto { @MaxLength(256) remarks: string; }

在控制器里面就要限制用户传入的更新数据类型必须与dto相同/share/src/modules/goods/goods.controller.ts:
@Put(':id') updata(@Param('id') id: string, @Body() updateRoleDto: UpdataGoodsDto) { return this.goodsService.updata(id, updateRoleDto); }

先找到对应的数据, 再进行数据的更新/share/src/modules/goods/goods.service.ts
async updata(id, updataGoodsDto: UpdataGoodsDto) { const goods = await this.goodsRepository.findOne(id) Object.assign(goods, updataGoodsDto) return this.goodsRepository.save(goods) }

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

6. 一对一关系
???? 同数据库里的一对一关系, 比如一个商品对应一个秘密厂家, 厂家是单独一张表, 一起来做下吧(这里比喻不恰当, 当前现实意义不是重点):
nest g module modules/mfrs nest g controller modules/mfrs nest g service modules/mfrs

/share/src/modules/mfrs/entity/mfrs.entity.ts
import { Entity, Column, Timestamp, CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Mfrs { @PrimaryGeneratedColumn('uuid') id: number; @Column() msg: string; @CreateDateColumn() createDate: Timestamp; }

  1. 这里我定义了uuid的加密类型。
在我们的商品表里面/share/src/modules/goods/entity/goods.entity.ts加上一个与mfrs表对应的行:
@OneToOne(() => Mfrs) @JoinColumn() mfrs: Mfrs

  1. 在你的表里生成的列不叫mfrs而是叫mfrsId
goods模块引入mfrs模块:
第一步: 从mfrs模块文件导出exports: [MfrsService] 第二步: 在goods的模块文件中引入imports: [MfrsModule] 第三步: 在goods.service.ts的class类中注入mfrs的服务, private readonly mfrsService: MfrsService, 在我们创建商品时, 把这个mfrs信息也插入进去:
async create(body) { const { name, remarks } = body; const goodsDto = new CreateGoodsDto(); goodsDto.name = name; goodsDto.remarks = remarks; const mfrs = await this.mfrsService.create({ msg: `${name}: 是正品` }); goodsDto.mfrs = mfrs; return this.goodsRepository.save(goodsDto) }

搜索对应关系 ???? 比如我直接用find方法查找goods表, 并没有查找出mfrs的信息, 因为我们需要配置相关的参数才可以:
this.goodsRepository.findAndCount({ relations: ['mfrs'] })

写给初用Nestj做项目的你(四篇:|写给初用Nestj做项目的你(四篇: TypeORM操作mysql数据库, 内附坑点罗列)
文章图片

7. 多对一, 与一对多关系
假设一个商品goods对应一个样式style, 一个style对应多个商品就可以写成如下形式:
goods.entity.dto里面添加设配置:
@ManyToOne(() => Style, style => style.goods) style: Style;

style.entity.dto里面添加设配置:
@OneToMany(() => Goods, goods => goods.style) goods: Goods[];

create-goods.dto.ts里面增加如下, 这样才能正常的创建新的goods:
@IsOptional() style: Style;

创建goods时如此改动:
async create(body) { const { name, remarks, styleId } = body; const goodsDto = new CreateGoodsDto(); goodsDto.name = name; goodsDto.remarks = remarks; const mfrs = await this.mfrsService.create({ msg: `${name}: 是正品` }); goodsDto.mfrs = mfrs; // 此处新增关联关系 goodsDto.style = await this.mtyleService.findOne(styleId) return this.goodsRepository.save(goodsDto) }

8. 多对多关系
???? 多对多与上面差别也不大, 但有一个细节值得注意, 比如你用a表与b表多对多关联,则会产生一张名为a_b的表, 当储存的时候a.b = [b1, b2]这个样子。
9. build语句, 处理更复杂场景
???? find很简洁好看, 但它无法应对所有的场景:
QueryBuilder是 TypeORM 最强大的功能之一 ,它允许你使用优雅便捷的语法构建 SQL 查询,执行并获得自动转换的实体, 简单理解其就是一种美观上不如find但是比find能做的事要多的方法。
this.goodsRepository.createQueryBuilder('goods')就可以创建出来。
比如一个goods商品
  1. goods有 name名称, keywords关键字两种属性, 并且这两个属性都是单独的表我们需要去关联, 此时我们需要模糊匹配功能。
  2. (重点)goods有一个属性maintainers是一个维护者的集合, 为数组类型, 大概长这样[{id:1, name:'张三'}, {id:2, name:'李四'}]
  3. (重点) 比如当前用户的id为9,我们需要剔除掉maintainers数组中的id不为9的数据。
这个语句大概的样子是这样的:
const qb = this.goodsRepository .createQueryBuilder('goods') .leftJoinAndSelect('goods.keywords', 'goods_keyword') .leftJoinAndSelect('goods.name', 'goods_name') .leftJoinAndSelect('goods.maintainers', 'user'); const { keyword, name } = query; qb.where('goods.keyword LIKE :keyword', { keyword: `%${keyword}%` }); qb.orWhere('goods.name LIKE :name', { name: `%${name}%`, }); // 这里的'user.id'指的是'user'表里面查出的数据 qb.andWhere('user.id = :id', { id: 9 }); const [list, total] = await qb.getManyAndCount();

end.
???? 这次就是这样, 快去突破自我吧, 希望和你一起进步。

    推荐阅读