前言
nestjs作为一个类似java spring的框架,将ioc 容器、AOP编程贯彻到了极致。那么如何用nestjs实现一套增删查改呢?本文将为大家一一讲解。
目标
- 实现增删查改
- 增加和修改时,自动注入字段,自动赋值createId(创建人)、createDt(创建时间)、updateId(更新人)、updateDt(更新时间)
- 实现分页查询
实现步骤
1.使用TypeOrmModule连接数据库
为了与 SQL和 NoSQL 数据库集成,Nest 提供了 @nestjs/typeorm 包。Nest 使用TypeORM是因为它是 TypeScript 中最成熟的对象关系映射器( ORM )。因为它是用 TypeScript 编写的,所以可以很好地与 Nest 框架集成。
在APPModule中,引入typeormmodule
通常数据库的配置都保存在配置文件里,这里我们使用工厂功能 useFactory来定制连接数据库
注:本项目使用mysql作为数据库
import { Module } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleAsyncOptions } from '@nestjs/typeorm';
import { ApplicationModule } from 'modules/application/application.module';
import { ConfigModule, ConfigService } from './../config';
......略
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return {
type: configService.get('DB_TYPE'),
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
entities: [__dirname + './../**/**.entity{.ts,.js}'],
synchronize: configService.isEnv('dev'), // 如果开启,就会自动根据扫描到的实体类,应用到数据库表结构上
} as TypeOrmModuleAsyncOptions;
},
}),
ConfigModule,
AuthModule,
ApplicationModule,
]
})
export class AppModule { }
2.定义实体Entity
数据库连上后,由于在开发模式下开启了数据库表结构同步。因此,实体创建好之后,在nest服务启动时就会执行sql,创建好对应的表和键。
接下来在代码里讲解如何创建实体类:
// ApplicationEntity.ts
import { BaseEntity } from "../../support/code/";
import { IsNotEmpty } from "class-validator";
import { Column, Entity } from "typeorm";
import { ApiProperty } from "@nestjs/swagger";
@Entity('application') // 这里可以自定义数据库表名
export class ApplicationEntity extends BaseEntity {
@ApiProperty({
description: '应用名称'
}) // api文档中用到
@Column({ length: 255, comment: '应用名称' }) // 数据库表中的字段配置以及备注,更多配置(例如transformer)可以参考官方文档
@IsNotEmpty({ message: '应用名称不能为空' }) //用户传参过来时做字段验证用,详见ValidatePipe
public name: string;
}
// BaseEntity.ts
// 所有的业务实体都继承BaseEntity在数据库里设置id和log字段
import { v4 } from 'uuid';
import { Column, PrimaryColumn } from "typeorm";
import { Exclude, Expose, Transform } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
export class BaseEntity {
@ApiProperty({
description: 'ID'
})
@PrimaryColumn() //所有的表都以id为主键,值为uuid
id: string = v4();
@Column({ type: 'datetime', nullable: true })
@Transform(v => +v)
createDt: Date;
@Column({ type: 'datetime', nullable: true })
@Transform(v => +v)
updateDt: Date;
@Exclude()
@Column({ type: 'varchar', nullable: true })
createId: string;
@Exclude()
@Column({ type: 'varchar', nullable: true })
updateId: string;
}
3.引入Entity
在ApplicationModule中,将
“`ApplicationEntity “`类传递给“` TypeOrm.forFeature“`函数供Nest实例化,在开启“`synchronize“`的时候,Nest启动时,还会创建/更新数据库表。
注意在生产环境下不要打开同步,否则容易出现错误的代码,错误地触发同步导致数据库表损坏,数据丢失的情况。
4.Service层实现CRUD
4.1 InjectRepository
我们已经将Entity实例化了,并且由 forFeature() 方法定义在当前范围中注册哪些存储库,接下来就是在service层引入数据库:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PageFindConditions } from '../../support/page/page.findConditions';
import { FindConditions, Like, Repository } from 'typeorm';
import { ApplicationEntity } from './application.entity';
@Injectable()
export class ApplicationService {
constructor(
@InjectRepository(ApplicationEntity) // 使用 @InjectRepository()装饰器将 applicationRepository 注入到 ApplicationService 中
private readonly applicationRepository: Repository<ApplicationEntity>
) { }
4.2 CUD
这里没什么好说的,直接贴代码
async add(query: ApplicationEntity) {
delete query.id
return await this.applicationRepository.insert(this.applicationRepository.create(query))
}
async update(query: ApplicationEntity) {
return await this.applicationRepository.update(query.id, this.applicationRepository.create(query))
}
async delete(id: string) {
return await this.applicationRepository.delete(id)
}
async list(query: ApplicationEntity): Promise<ApplicationEntity[]> {
const params = this.applicationRepository.create(query);
delete params.id;
return await this.applicationRepository.find(params)
}
注意点: 这里先create后执行操作,是为了防止传参有非预期的key(多余的key或不希望被传进来的key)被透传到后面的处理中,影响结果。
比如add操作中,不希望用户执行传参id过来,create时就会重新创建实例,此时会设置id为一个uuid。
4.3 分页查询
分页查询,支持传参过滤,支持分页参数过滤
使用findAndCount,传参分别有
– 查询条件where
– 分页参数: offset偏移值skip
,分页大小
“`take“`
– 排序条件 “`order“`
如果在业务中,需要模糊匹配名称,那么可以使用
“`Like()“`做模糊匹配
service层查询出来的结果,就是分页的数据,还有总页码数的信息了。
async page(pageParams, query: ApplicationEntity): Promise<[ApplicationEntity[], number]> {
const { id, ...params } = this.applicationRepository.create(query);
const where: FindConditions<ApplicationEntity> = {
...params,
}
if (where.name) {
where.name = Like(`%${where.name}%`)
}
const conditions = new PageFindConditions(where, pageParams)
return await this.applicationRepository.findAndCount(conditions)
}
// PageFindConditions.ts
export class PageFindConditions {
public where;
public skip: number;
public take: number;
public order: any;
constructor(where, { pageSize = 10, pageNum = 1 }) {
this.where = where;
this.skip = (pageNum - 1) * pageSize;
this.take = pageSize;
this.order = {
updateDt: 'DESC',
createDt: 'DESC'
}
// Object.assign(this, {
// where,
// skip: (pageNum - 1) * pageSize,
// take: pageSize,
// order: {
// updateDt: 'DESC',
// createDt: 'DESC'
// }
// })
}
}
Q: 如果换了个业务,它的分页传参名不是pageSize和pageNum,那么岂不是需要对代码的Service层大改?
A: 并不是的。这里在Controller层做了处理,保证传到Service层时的参数就是pageSize和pageNum
5.Controller层调用Service
5.1 创建Page装饰器,处理分页参数
读取query中的字段,返回为pageSize,pageNum。如果query中为其他的字段名,则在这里做统一处理。在controller层只需要@Page() pageQuery即可获取到分页参数。
const Page = createParamDecorator(
(data: string, ctx: ExecutionContext): PagePayload => {
const request = ctx.switchToHttp().getRequest();
const { pageSize, pageNum } = request.query;
return {
pageSize,
pageNum,
};
},
);
5.2 使用ClassSerializerInterceptor过滤响应数据中的敏感字段
官网描述:
在发送实际响应之前, Serializers 为数据操作提供了干净的抽象层。例如,应始终从最终响应中排除敏感数据(如用户密码)。此外,某些属性可能需要额外的转换,比方说,我们不想发送整个数据库实体。相反,我们只想选择 id 和 name 。其余部分应自动剥离。不幸的是,手动映射所有实体可能会带来很多麻烦。
大概步骤就是 在controller中使用装饰器@UseInterceptors(ClassSerializerInterceptor)
,
在entity中,对想要过滤的字段,使用@Exclude()
装饰器。
由于装饰器并不能注入到obj中,返回的结构不能使用obj,要全部使用class才能达到被转换过滤的效果。
@Get('page')
@UseGuards(JwtAuthGuard)
@UseInterceptors(ClassSerializerInterceptor) // Serializers为数据操作提供了干净的抽象层。例如,应始终从最终响应中排除敏感数据(如用户密码)
async page(@Page() pageQuery: PagePayload, @Query() query): Promise<ApiResult> {
const [rows, total] = await this.applicationService.page(pageQuery, query);
// 使用class目的是能用上ClassSerializerInterceptor
return ApiResult.PAGE<ApplicationEntity>({ rows, total })
}
// ApiResult
public static PAGE<T>({ rows, total }: PageResult<T>, message: string = '拉取分页成功') {
return new ApiResult(200, message, { rows, total });
}
添加修改数据时日志字段值的注入
Q:创建/更新者id,创建/更新时间等log字段是如何注入的?
A:对增加和更新接口,使用Pipe,对传参进行转换
STEP 1:创建管道类CreatePipe,并且注入Request,注意是引入自'@nestjs/core
包
import { REQUEST } from '@nestjs/core';
@Injectable()
export class CreatePipe implements PipeTransform {
constructor(@Inject(REQUEST) private readonly request) { }
transform(value: any, metadata: ArgumentMetadata) {
value.createDt = new Date();
value.createId = this.request?.user?.id
return value;
}
}
STEP 2:将管道实例绑定到路由参数中
@Post('add')
@UseGuards(JwtAuthGuard)
async add(@Body(CreatePipe) query: ApplicationEntity) {
return ApiResult.SUCCESS(await this.applicationService.add(query), '添加成功')
}
在方法中,获取到
“`query“`的时候就已经是带上用户id和时间的实体啦。
不知道使用装饰器是否也能达到同样的效果,可以试一试。
总结
在实现了一个常规的通用CRUD中,应用到了 自定义装饰器,管道,
“`Inject()“`注入,数据库绑定、ValidatePipe等技术点,更加深了对Nest这个新型框架的了解。