写给初用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,
}),],
// ...
- 下面演示的是链接我本地的
mysql
,database
是库名。 - 能够在
imports
外面定义多个TypeOrmModule.forRoot
能够操作多个库, 多个时还须要填写不同的name
属性。 synchronize
主动载入的模型将同步。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;
}
@PrimaryGeneratedColumn()
装璜了id为主键, 类型为数字。@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 { }
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()
}
}
@InjectRepository()
装璜器将goodsRepository
注入GoodsService
中。- 被注入进来的
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呈现了, 情景步骤如下:
- 我要写一个
user
的实体。 - 我间接复制了
goods.entity.ts
实体的文件改名为user.entity.ts
。 - 批改其外部的属性, 比方定义了
userName
,age
,status
等新属性, 删除了商品价格等旧属性。 - 然而咱们还没有把导出的
Goods
类名改成User
, 因为编辑器失去焦点等起因导致vscode
主动保留了。 - 惊喜来了, 你的
goods
表被清空了
, 是的你还没有在任何中央援用这个user.entity.ts
文件, 然而它曾经失效了, 并且无声无息的把你的goods
表清空了。 - 我过后问该项目标负责人如何防止上述问题, 他钻研了一下午, 通知我敞开主动保留…(告辞)
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;
}
nullable: false
不能够为空。unique: true
惟一值, 不容许有反复的name
的值呈现, 须要留神的是如果以后的表外面曾经有反复的name
了typeorm
会报错, 所以如果设置失败请检查表内容。length: 256
限度字符的长度, 对应varchar(256)
。default: '暂无'
默认值, 要留神当你手动设置为空字符串时并不会被设置为默认值。type: 'enum
定义为枚举类型,enum: GoodsStatus
指定枚举值, 当你赋予其非枚举值时会报错。type: 'timestamp'
定义类型为工夫格局,CURRENT_TIMESTAMP
默认就是创立工夫。@CreateDateColumn()
这个主动就能够为咱们设置值为创立工夫。@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的项, not
id不是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;
}
- 这里我定义了
uuid
的加密类型。
在咱们的商品表外面/share/src/modules/goods/entity/goods.entity.ts
加上一个与mfrs
表对应的行:
@OneToOne(() => Mfrs)
@JoinColumn()
mfrs: Mfrs
- 在你的表里生成的列不叫
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
商品
- goods有
name名称
,keywords关键字
两种属性, 并且这两个属性都是独自的表咱们须要去关联, 此时咱们须要含糊匹配性能。 - (
重点
)goods有一个属性maintainers
是一个维护者的汇合, 为数组类型, 大略长这样[{id:1, name:'张三'}, {id:2, name:'李四'}]
。 - (
重点
) 比方以后用户的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.
这次就是这样, 快去冲破自我吧, 心愿和你一起提高。
发表回复