Skip to content

nestjs结合typeorm实践

一. typeORM 数据库模块

1. 下载依赖

js
// 数据库模块相关
npm install --save @nestjs/typeorm typeorm mysql2

// 配置文件相关
npm i --save @nestjs/config

2. 自动加载配置

在写数据库配置之前,先创建一个环境变量配置,因为数据库用户密码等配置需要从配置文件内动态读取

2.1 根目录下新建.env文件

bash
# .env
# 应用配置
APP_ROUTER_PIX=/api # 路由前缀
APP_PORT=4000   # 应用端口

# Type ORM 专有变量
# 详情:https://typeorm.io/#/using-ormconfig
TYPEORM_TYPE=mysql
TYPEORM_DATABASE=nest_demo
TYPEORM_HOST=127.0.0.1
TYPEORM_PORT=3306
TYPEORM_USERNAME=root
TYPEORM_PASSWORD=root

# Redis 配置
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

2.2 新建config/configuration.ts文件, config目录可以根据自己的需要建在项目任何位置,一般为根目录或者/app/config

ts
// config/configuration.ts
const env = process.env;

export default () => {
  return {
    app: {
      port: parseInt(env.APP_PORT, 10) || 4000,
    },
    db: {
      type: env.TYPEORM_TYPE,
      database: env.TYPEORM_DATABASE,
      host: env.TYPEORM_HOST,
      port: parseInt(env.TYPEORM_PORT, 10) || 3306,
      username: env.TYPEORM_USERNAME,
      password: env.TYPEORM_PASSWORD,
    },
    redis: {
      host: env.REDIS_HOST,
      port: env.REDIS_PORT,
    },
  };
};

2.3 加载配置

ts
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import loadConfig from './config/configuration';
@Module({
  import:[
    // 配置
  ConfigModule.forRoot({
    load: [loadConfig],
    envFilePath: ['.env'], // 可以根据不同环境变量加载不同的env文件
  }),
  ]
})

3. 配置数据库

3.1 创建数据库实体xxx.entity.ts(可以看做是数据库模型)

ts
// app.module.ts

import {
  BeforeInsert,
  Column,
  CreateDateColumn,
  Entity,
  Index,
  JoinTable,
  ManyToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Role } from 'src/modules/role/entities/role.entity';

export enum UserStatus {
  NORMAL = 1,
  INVALID = 2,
}

@Entity()
export class User {
  @PrimaryGeneratedColumn({ type: 'int', comment: '自增ID' })
  id: number;
  @Index()
  @Column({ type: 'varchar', length: 50, comment: '用户名' })
  username: string;

  @Column({ type: 'varchar', length: 100, comment: '密码' })
  password: string;

  @Column({ type: 'varchar', length: 50, comment: '昵称' })
  nickname: string;

  @Column({
    type: 'int',
    comment: '状态1-正常 2-失效',
    default: UserStatus.NORMAL,
  })
  status: number;

  @Index()
  @Column({ type: 'varchar', length:20, comment: '手机号' })
  tel: string;

  @Index()
  @Column({ type: 'varchar', length: 50, comment: '邮箱' })
  email: string;

  @CreateDateColumn()
  createdTime: Date;

  @UpdateDateColumn()
  updatedTime: Date;

  @BeforeInsert()
  private async hashPassword() {
    const salt = await bcrypt.genSalt();
    this.password = await bcrypt.hash(this.password, salt);
  }

  // 关联关系
  @ManyToMany(() => Role, (role) => role.users)
  @JoinTable()
  roles: Role[];
}

3.2 加载连接数据库模型

ts
@Module({
  imports:[
    // 加载连接数据库模型
  TypeOrmModule.forRootAsync({
    imports: [ConfigModule],
    inject: [ConfigService],
    useFactory: (configService: ConfigService) => {
      const { type, host, port, username, password, database } =
        configService.get('db');
      return {
        // .env 获取
        type,
        host,
        port,
        username,
        password,
        database,
        entities: ['dist/modules/**/entities/*.entity{.ts,.js}'],
        synchronize: true,
      };
    },
  }),
  ....
  ]
})

3.3 合并app.module.ts文件

ts
// app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './modules/user/user.module';
import { RoleModule } from './modules/role/role.module';
import { ResourceModule } from './modules/resource/resource.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import loadConfig from './config/configuration';
import { TypeOrmModule } from '@nestjs/typeorm';

const businessModules = [UserModule, RoleModule, ResourceModule];
const libModules = [
  // 配置
  ConfigModule.forRoot({
    load: [loadConfig],
    envFilePath: ['.env'], // 可以根据不同环境变量加载不同的env文件
  }),
  // 加载连接数据库模型
  TypeOrmModule.forRootAsync({
    imports: [ConfigModule],
    inject: [ConfigService],
    useFactory: (configService: ConfigService) => {
      const { type, host, port, username, password, database } =
        configService.get('db');
      return {
        // .env 获取
        type,
        host,
        port,
        username,
        password,
        database,
        entities: ['dist/modules/**/entities/*.entity{.ts,.js}'],
        synchronize: true,
      };
    },
  }),
];

@Module({
  imports: [...libModules, ...businessModules],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

4. 使用

4.1 对应模块里面注册

ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

该模块使用该TypeOrmModule.forFeature()方法来定义在当前范围内注册了哪些存储库。有了它,我们可以使用装饰器将其UsersRepository注入:UsersService @InjectRepository()

4.2 对应模块的service里面注入,并使用

ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto) {
    return await this.usersRepository.insert(createUserDto);
  }
}

4.3 触发@BeforeInsert@BeforeUpdate方法

当我们需要在保存数据时候,对某个字段的数据进行处理,就需要用到@BeforeInsert@BeforeUpdate装饰器, 但是,直接使用save或insert方法,并不会触发这两个装饰器装饰的方法。 正确方式

ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/request.dto';
import { UpdateUserDto } from './dto/success.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  create(createUserDto: CreateUserDto) {
    /* 触发@beforeInsert 和beforeUpdate的两种方式 */
    /* 方式一 */
    // const { username, password, email, nickname, tel } = createUserDto;

    // const user = new User();
    // user.username = username;
    // user.password = password;
    // user.nickname = nickname;
    // user.tel = tel;
    // user.email = email;
    // return this.usersRepository.save(user);

    /* 方式二 */
    const entityDto = this.usersRepository.create(createUserDto);
    return this.usersRepository.save(entityDto);
  }
}

这里贴上一篇文章: https://www.cnblogs.com/boyGdm/p/15707636.html订阅者和监听者装饰器

4.4 查询时,忽略某个字段返回,例如密码

  1. 安装依赖class-transformer
bash
npm i class-transformer
  1. 在实体里面引入,并使用装饰器
ts
import { Exclude } from 'class-transformer'; // 引入
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 500 })
  username: string;

  @Exclude() // 使用
  @Column({ length: 500 })
  password: string;
}
  1. 在controller里面使用
ts
// 引入
import { Controller, Get, UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @UseInterceptors(ClassSerializerInterceptor) // 使用
  @Get()
  findAll() {
    return this.userService.findAll();
  }
}

二. swagger自动生成接口文档

官网地址:https://docs.nestjs.com/v8/openapi/introduction

1. 安装依赖

由于我这里们底层框架用的express, 因此需要安装swagger-ui-express,用于渲染UI界面

sh
npm install --save @nestjs/swagger swagger-ui-express

2. main.ts文件配置

ts
// 引入
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  /* swagger 配置*/
  const config = new DocumentBuilder()
    .setTitle('nest-demo API接口文档')
    .setDescription('nest-demo项目api接口文档')
    .setVersion('1.0')
    // .addTag('cats')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);  // 'api'是接口文档路由前缀

  await app.listen(PORT);
}

3. controller里面配置

这里以用户注册模块为例

ts
import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';

@ApiBearerAuth() // 加了它,文档里面某些接口就需要权限
@ApiTags('用户模块') // 分组用
@Controller('admin/user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post('register')
  @ApiBody({ type: CreateUserDto }) // body入参说明,下一步会说到
  @ApiOperation({ summary: '用户注册' }) // 接口名字
  @ApiResponse({ status: 200, description: '注册成功' }) // 响应示例
  async create(@Body() createUserDto: CreateUserDto) {
    return await this.userService.create(createUserDto);
  }
}

4. 规定入参字段及示例

在入参文件request.dto.ts文件里面,注册部分的入参上面加上swigger装饰器,就能在文档里面根据dto规则生成入参scheam

ts
// request.dto.ts

import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ example: 'zhangsan', description: '用户名' })
  username: string;

  @ApiProperty({ example: '123456', description: '密码' })
  password: string;

  @ApiProperty({ example: '法外狂徒张三', description: '昵称' })
  nickname: string;

  @ApiProperty({
    example: 'http://abc.jpg',	// 字段示例
    description: '头像地址',	// 字段说明
    required: false,		// 表示非必传
  })
  avatar?: string;

  @ApiProperty({ example: 18888888888, description: '手机号' })
  tel?: string;

  @ApiProperty({ example: '1234562qq.com', description: '邮箱' })
  email: string;
}

三. class-validator参数校验

nestjs里面有一个管道的概念,官方文档也介绍了管道的两个典型作用:

  • 转换:将输入数据转换为所需的形式(例如,从字符串到整数)
  • 验证:评估输入数据,如果有效,则简单地通过不变;否则,当数据不正确时抛出异常

其中, 转换很好理解, 就是 "123456" => 123456, nest内置了9个开箱即用的管道,如下:

ts
import {
    ValidationPipe,
    ParseIntPipe,
    ParseFloatPipe,
    ParseBoolPipe,
    ParseArrayPipe,
    ParseUUIDPipe,
    ParseEnumPipe,
    DefaultValuePipe,
    ParseFilePipe
} from "@nestjs/common"

这里主要介绍ValidationPipe(校验pipe),转换管道的使用很简单,只需要引入后

ts
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

下面介绍参数的校验:

1. 先安装依赖

sh
npm i --save class-validator class-transformer

2. 在入参dto文件内使用校验装饰器

ts
// request.dto.ts

import { ApiProperty } from '@nestjs/swagger';
import {	// 参数校验装饰器引入
  IsAlphanumeric,
  IsString,
  IsUrl,
  IsEmail,
  IsNumberString,
  Length,
  MinLength,
  MaxLength,
  IsDefined,
  IsOptional,
} from 'class-validator';

export class CreateUserDto {
  @ApiProperty({ example: 'zhangsan', description: '用户名' })
  @IsAlphanumeric() // 检查字符串是否只包含字母和数字
  @Length(3, 20, {
    message: '用户名应该在3-20字符之间',
  })
  username: string;

  @ApiProperty({ example: '123456', description: '密码' })
  @IsString()
  @MinLength(6, {
    message: '密码最少6位',
  })
  @MaxLength(20, {
    message: '密码最长不超过20位',
  })
  password: string;

  @ApiProperty({ example: '法外狂徒张三', description: '昵称' })
  @IsString()
  @Length(1, 20, {
    message: '昵称名应该在1-20字符之间',
  })
  nickname: string;

  @ApiProperty({
    example: 'http://abc.jpg',
    description: '头像地址',
    required: false,
  })
  @IsUrl()
  @IsOptional() // 该字段是非必传的, 改装饰器可用于在没传字段时忽略校验  检查给定值是否为空(=== null,=== undefined),如果是,则忽略该属性上的所有验证器
  avatar?: string;

  @ApiProperty({ example: 18888888888, description: '手机号' })
  @Length(11, 11, {
    message: '应为11位数字手机号',
  })
  @IsNumberString() // 是否是字符串形式的数字
  @IsOptional()
  tel?: string;

  @ApiProperty({ example: '1234562qq.com', description: '邮箱' })
  @IsEmail()
  email: string;
}

对于class-validator里面的装饰器, 可以查看下面文章: nest class-validator验证修饰器中文文档https://github.com/typestack/class-validator#passing-options

3.在main.ts中注册全局管道

使用内置的 ValidationPipe

ts
// main.ts
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({whitelist:true})); // whitelist 如果设置为 true,验证器将删除任何不使用任何验证装饰器的属性的已验证(返回)对象。
  await app.listen(3000);
}
bootstrap();