主题
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 查询时,忽略某个字段返回,例如密码
- 安装依赖
class-transformer
bash
npm i class-transformer
- 在实体里面引入,并使用装饰器
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;
}
- 在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自动生成接口文档
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
中注册全局管道
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();