关于前端:写给初用Nestj做项目的你四篇-TypeORM操作mysql数据库-内附坑点罗列

7次阅读

共计 10154 个字符,预计需要花费 26 分钟才能阅读完成。

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

以后的数据库:

创立模块
// 控制台里输出创立命令
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 办法前面会举例出更多。

二. 坑点列举 ( 重点)

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

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

     以咱们下面设置的实体为例:

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

    @Column()
    name: string;
}

     咱们初始化的表外面 name 字段对应的类型是 varchar(45), 然而name: string; 这种形式初始化的类型是 varchar(255), 此时类型是不统一的, typeorm 抉择清空咱们的 name 列, 是的你没听错name 列 被清空了:

     并且是只有你运行 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 处举荐如此应用, 几乎无语。

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

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

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

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

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

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

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

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

9. findOne

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

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

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

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

更新

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

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

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.

     这次就是这样, 快去冲破自我吧, 心愿和你一起提高。

正文完
 0