31

写给初用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 模型将自动载入。

当前的数据库:
image.png

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

/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<Goods>
    ) { }
    getList() {
        return this.goodsRepository.find()
    }
}
  1. @InjectRepository()装饰器将goodsRepository注入GoodsService中。
  2. 被注入进来的Repository都自带属性, 这里使用了自带的find方法后面会举例出更多。

image.png

二. 坑点罗列(重点)

     满纸荒唐言, 一把辛酸泪, 当时我被坑的不浅。

1. 实体的强替换, 莫名删表 (坑人指数 ⭐️ ⭐️ ⭐️ ⭐️)

     以我们上面设置的实体为例:

export class Goods {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;
}

     我们初始化的表里面name字段对应的类型是varchar(45), 但是name: string;这种方式初始化的类型是varchar(255), 此时类型是不一致的, typeorm选择清空我们的name列, 是的你没听错name列被清空了:

image.png

     并且是只要你运行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处推荐如此使用, 简直无语。
image.png

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<Goods>
    ) { }
1. 无条件查询所有数据

this.goodsRepository.find()查询goods表的全部数据, 以及每条数据的信息。

2. 只显示name, createDate两列数据:
this.goodsRepository.find({
       select: ['name', 'createDate']
    })

image.png

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

image.png

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

image.png

5. 排序, 以name降序, 创建时间升序排列
this.goodsRepository.find({
    order: {
         name: "DESC",
         createDate: "ASC"
    }
})
6. 切割, skip跳过1条, take取出3条
this.goodsRepository.find({
     skip: 1,
     take: 3
})

image.png

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

image.png

8. findAndCount 把满足条件的数据总数返回

数据是数组形式, [0]是匹配到的数组, [1]是符合条件的总数可能与[0]的长度不相同。

this.goodsRepository.findAndCount({
       select: ['name']
});

image.png

9. findOne

只取配到的第一条, 并且返回形式为对象而非数组:

  this.goodsRepository.findOne({
     select: ['name']
  });

image.png

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
        }
    }

image.png

五. 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)
    }

image.png

更新

老样子, 先建立一份更新的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)
    }

image.png

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']
 })

image.png

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.

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


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者