nestjs入门记之「实现CRUD」

前言

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,对传参进行转换

Nest中文文档之转换管道

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这个新型框架的了解。

nestjs入门记之「实现CRUD」
滚动到顶部