文章目录
NestJS 构建下一代 Node.js 应用
2023.11月的某一天,我在学习小满哥在B站的视频,无意中发现Nestjs这个东西,然后就跑进知乎,在乎uu的介绍和描述,我了解到了这个新奇的东西——像极了
Spring
。我在此前学习过
Express
、Koa
的Nodejs
框架,他们都是用前端的语言来写服务端,缺少了后端的如IOC控制反转
、DI依赖注入
、AOP面向切片编程
等等思想的实现(大神可以手写模拟实现,我是菜鸡我不行,以后会试试,毕竟是非常优秀的思想),而我暂时又不想学Java
的像SpringBoot
的框架系列的技术,这时候我想Nestjs
是一个不错的选择。以下是我学习小满哥的
Nestjs
视频的文档记录,当然也会有一些我的见解。
介绍
Nestjs
是一个基于Express
和Fastify
做的框架,集成了TypeScript
,实现了各种特性,继承了两大框架的优点和生态。特别是nest/cli
的一系列命令,能够让你快速搭建服务。
- 基于
Express
和Fastify
框架
NestJS
构建在Express
和 Fastify
之上,这两者都是 Node.js
中非常流行的 HTTP 框架。这使得 NestJS
具有广泛的社区支持和成熟的中间件生态系统。你可以选择使用其中一个或者根据需要在它们之间切换。
- 集成
TypeScript
NestJS
是一个使用 TypeScript
编写的框架,这意味着你可以利用 TypeScript
的强类型系统、面向对象的编程范式以及其他高级语言特性。TypeScript
让你在编写代码时能够更早地捕捉潜在的错误,提高了代码的可读性和可维护性。
- 依赖注入和模块化
通过依赖注入,你可以更灵活地组织你的代码,使得组件更加可测试和可维护。NestJS
使用模块的概念,允许你将应用程序拆分为一系列模块,每个模块都负责一个特定的功能。模块化的设计使得应用程序的组织结构更加清晰,也方便团队协作。
初始化项目
-
脚手架安装
npm install -g @nestjs/cli
-
创建项目
nest new <project-name>
拦截器(Interceptor)
通过以下代码的学习,不难看出拦截器和管道校验都有
Express
中间件的影子。
响应拦截器实现
-
在
src
目录下新建common
文件夹,文件夹下创建response.ts
文件import { CallHandler, Injectable, NestInterceptor } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; interface Data<T> { data: T; } // 响应拦截器根据模板返回数据 @Injectable() export class Response<T> implements NestInterceptor { intercept(context: any, next: CallHandler): Observable<Data<T>> { return next.handle().pipe( map((data) => { return { code: 200, data, message: '冲就完事了', success: true, }; }) ); } }
-
使用全局响应拦截器
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Response } from './common/response'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局注册响应拦截器 app.useGlobalInterceptors(new Response()); await app.listen(3000); } bootstrap();
异常拦截器实现
-
common
文件夹下创建filter.ts
文件import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; import { Request, Response } from 'express'; // 异常拦截器 @Catch(HttpException) export class HttpFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const { getResponse, getRequest } = host.switchToHttp(); const response = getResponse<Response>(); const request = getRequest<Request>(); const status = exception.getStatus(); // 优先取全局的异常管道符校验失败的响应信息,前提是使用了全局的校验管道 const message = exception.getResponse() || exception.message; response.status(status).json({ status, data: message, success: false, time: new Date(), path: request.url, }); } }
-
使用全局异常拦截器
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Response } from './common/response'; import { HttpFilter } from './common/filter'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局注册异常拦截器 app.useGlobalFilters(new HttpFilter()); // 全局注册响应拦截器 app.useGlobalInterceptors(new Response()); await app.listen(3000); } bootstrap();
管道(Pipe)
类型转换管道
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { AppService } from './app.service';
// pipe系列
// ValidationPipe // 验证
// ParseIntPipe // int 整型类型 -- number
// ParseFloatPipe // float 浮点类型
// ParseBoolPipe // boolean 布尔类型
// ParseArrayPipe // array 数组类型
// ParseUUIDPipe // UUID 类型
// ParseEnumPipe // enum 枚举类型
// DefaultValuePipe // any 默认值类型
// ...还有什么文件类型等等,可以进行转换
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get(':id')
getHello(@Param('id', ParseIntPipe) id: number): any {
console.log('id type ==> ' + typeof id); // out: id type ==> number
return this.appService.getHello();
}
}
类型校验管道
有两种实现方式:
1、利用两个库来实现颗粒度更细的校验。
2、使用全局的管道校验。
实现方式一
-
安装
class-transformer
和class-validator
pnpm i class-transformer class-validator --save
-
快速生成CRUD
nest g res login
-
快速生成pipe
nest g pi login
-
login.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; import { LoginService } from './login.service'; import { CreateLoginDto } from './dto/create-login.dto'; import { UpdateLoginDto } from './dto/update-login.dto'; import { LoginPipe } from './login.pipe'; @Controller('login') export class LoginController { constructor(private readonly loginService: LoginService) {} // 对接受的dto的对象进行管道处理 @Post('create') create(@Body(LoginPipe) createLoginDto: CreateLoginDto) { console.log(createLoginDto); return this.loginService.create(createLoginDto); } }
-
create-login.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; export class CreateLoginDto { @IsNotEmpty() @IsString() name: string; @IsNumber() age: number; }
-
login.pip.ts
import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; @Injectable() export class LoginPipe implements PipeTransform { // eslint-disable-next-line @typescript-eslint/no-unused-vars async transform(value: any, metadata: ArgumentMetadata) { // 将接收到的请求数据转换为指定的 DTO 实例 const DTO = plainToInstance(metadata.metatype, value); // 对实例中字段标注的装饰器进行校验,返回一个错误信息的数组 const errors = await validate(DTO); // 如果errors数组中存在元素,说明有字段类型校验失败 if (errors.length) { throw new HttpException(errors, HttpStatus.BAD_REQUEST); } return value; } }
实现方式二
全局的方法,则不需要再写方法一里的第一个和第二个文件了。
-
create-login.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; export class CreateLoginDto { @IsNotEmpty() @IsString() name: string; @IsNumber() age: number; }
-
main.ts
这里使用全局的管道进行类型的校验,只要传入的与标注的类型对不上,就会响应错误的消息。
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Response } from './common/response'; import { HttpFilter } from './common/filter'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局注册异常拦截器 app.useGlobalFilters(new HttpFilter()); // 全局注册响应拦截器 app.useGlobalInterceptors(new Response()); // 全局注册类型校验管道 搭配class-validator库使用 app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();
静态目录(Static Public)
这个简单,直接上代码!与Express没啥区别。做了一下TS的类型封装而已。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Response } from './common/response';
import { HttpFilter } from './common/filter';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
// 添加NestExpressApplication泛型,扩展属性的类型推导。
// 不然eslint会报错useStaticAssets不存在。
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 使用静态目录。src/public下的所有文件都可以直接访问
app.useStaticAssets(join(__dirname, '..', 'public'));
// 全局注册异常拦截器
app.useGlobalFilters(new HttpFilter());
// 全局注册响应拦截器
app.useGlobalInterceptors(new Response());
// 全局注册类型校验管道 搭配class-validator库使用
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
跨域(Cors)
跨域在Nestjs中有内置的一个方法可以实现,一行代码就解决了跨域,当然你也可以使用第三方库
pnpm i cors @types/cors --save
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Response } from './common/response';
import { HttpFilter } from './common/filter';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
// import * as cors from 'cors';
async function bootstrap() {
// 添加NestExpressApplication泛型,扩展属性的类型推导。
// 不然eslint会报错useStaticAssets不存在。
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 配置允许跨域
app.enableCors();
// 使用第三方库cors实现跨域
// app.use(cors());
// 使用静态目录。src/public下的所有文件都可以直接访问
app.useStaticAssets(join(__dirname, '..', 'public'));
// 全局注册异常拦截器
app.useGlobalFilters(new HttpFilter());
// 全局注册响应拦截器
app.useGlobalInterceptors(new Response());
// 全局注册类型校验管道 搭配class-validator库使用
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
守卫(Guard)
个人觉得,使用守卫做权限校验是十分不错的,全局的拦截器、管道、守卫,都有中间件的思想,只需要
new
一个实例过去,在特定时期会调用。当然也提供了局部的使用。
-
创建guard的测试类,使用命令快速生成CRUD
nest g res guard
-
创建角色守卫
nest g gu role
-
role.guard.ts
context
:请求守卫的上下文信息。其中,
context.getHandler()
可以获取到调用的Service
中的方法,配合Reflector
可以找到Controller
请求的路由方法的元数据,然后获取一些信息,如装饰器@SetMetaData
的值。context.switchToHttp()
看名称就知道,这是将上下文切换到HTTP
请求,在HTTP
请求中存在三个老朋友(Request
、Response
、Next
)。在
Request
中获取到Get
请求的query
中的值,接下来就可以判断了。import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Observable } from 'rxjs'; import type { Request } from 'express'; import { Reflector } from '@nestjs/core'; type TRoles = 'superAdmin' | 'admin' | 'user' | 'vipUser'; // 定义局部的请求守卫,权限校验 @Injectable() export class RoleGuard implements CanActivate { constructor(private ReflectorX: Reflector) {} canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const roles = this.ReflectorX.get<TRoles[]>('role', context.getHandler()); const req = context.switchToHttp().getRequest<Request>(); const { role } = req.query as { role: TRoles }; if (roles && !roles.includes(role)) return false; return true; } }
-
局部守卫实现(guard.controller.ts)
这里是对整个
/guard
路由下的使用了权限守卫,你也可以精确到某一个请求路由上,只是装饰器的位置不一样。import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, SetMetadata } from '@nestjs/common'; import { GuardService } from './guard.service'; import { CreateGuardDto } from './dto/create-guard.dto'; import { UpdateGuardDto } from './dto/update-guard.dto'; import { RoleGuard } from 'src/role/role.guard'; @Controller('guard') @UseGuards(RoleGuard) export class GuardController { constructor(private readonly guardService: GuardService) {} @Get('test') @SetMetadata('role', ['superAdmin', 'admin']) findAll() { return this.guardService.findAll(); } }
-
全局守卫实现(main.ts)
当注册了全局的守卫,所有的请求的都会走
RoleGuard
。这里可能同学们有一个疑问——如果我全局守卫和局部守卫同时使用会怎么样?
答:局部守卫优先级高于全局守卫,走过了局部守卫,如果为
true
,还会走全局守卫,这是一个流程。import { NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import { Response } from './common/response'; import { HttpFilter } from './common/filter'; import { ValidationPipe } from '@nestjs/common'; import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; // import * as cors from 'cors'; import { RoleGuard } from './role/role.guard'; async function bootstrap() { // 添加NestExpressApplication泛型,扩展属性的类型推导。 // 不然eslint会报错useStaticAssets不存在。 const app = await NestFactory.create<NestExpressApplication>(AppModule); // 配置允许跨域 app.enableCors(); // 使用第三方库cors实现跨域 // app.use(cors()); // 使用静态目录。src/public下的所有文件都可以直接访问 app.useStaticAssets(join(__dirname, '..', 'public')); // 全局注册异常拦截器 app.useGlobalFilters(new HttpFilter()); // 全局注册响应拦截器 app.useGlobalInterceptors(new Response()); // 全局注册类型校验管道 搭配class-validator库使用 app.useGlobalPipes(new ValidationPipe()); // 全局注册权限校验守卫,在controller的路由上使用装饰器@SetMetaData,然后在RoleGuard里面取值做判断 app.useGlobalGuards(new RoleGuard(new Reflector())); await app.listen(3000); } bootstrap();
接口文档构建(Swagger)
只写接口,不写接口文档的后端程序员,你们认为是一个合格的后端吗?我是忍不了🤬。
接下来使用将在
Nestjs
中使用Swagger来快速生成API
接口文档。
-
引入依赖
pnpm i @nestjs/swagger swagger-ui-express -D
-
配置并创建文档路由站点
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { // 添加NestExpressApplication泛型,扩展属性的类型推导。 // 不然eslint会报错useStaticAssets不存在。 const app = await NestFactory.create<NestExpressApplication>(AppModule); // swagger使用 const options = new DocumentBuilder() // 添加文档查看jwt校验 .addBearerAuth() // 设置文档标题 .setTitle('小胡的Nestjs-Stduy-Doc-Website') // 设置接口文档的描述 .setDescription('我是接口文档的描述') // 设置接口版本 .setVersion('1') // 打包 .build(); // 创建文档 const document = SwaggerModule.createDocument(app, options); // 安装,访问路径:http://localhost:3000/api-docs SwaggerModule.setup('api-docs', app, document); await app.listen(3000); } bootstrap();
-
接口中装饰器的使用
在这里可以查看更多用法:👉Swagger
import { Controller, Get, UseGuards, SetMetadata } from '@nestjs/common'; import { GuardService } from './guard.service'; import { RoleGuard } from 'src/role/role.guard'; import { ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; @Controller('guard') @UseGuards(RoleGuard) @ApiBearerAuth() // 权限,token校验 文档内的接口请求 @ApiTags('guard守卫的tag') // 这是/guard下的路由的标题 export class GuardController { constructor(private readonly guardService: GuardService) { } @Get('test') // 学习更多使用:https://docs.nestjs.com/openapi/introduction @ApiQuery({ name: 'role', required: true, description: '这是一个测试角色权限校验的一个路由值,query参数描述', }) @ApiParam({ name: 'test', description: 'api的param参数描述', }) // 返回的信息文档 @ApiResponse({ status: 400, description: '小黑子露出鸡脚了吧', }) // api怎么操作的一些描述 @ApiOperation({ summary: 'findAll接口', description: '描述啦啦啦', }) @SetMetadata('role', ['superAdmin', 'admin']) findAll() { return this.guardService.findAll(); } }
-
实体类属性字段上的装饰器使用
import { ApiProperty } from '@nestjs/swagger'; export class CreateGuardDto { // 更多可以查看SchemaObject类型定义的一些字段 @ApiProperty({ required: false, default: '小胡', }) name: string; @ApiProperty({ required: false, default: 18, }) age: number; @ApiProperty({ default: 'JK', type: String, required: true, }) hobby: string; }
思考
以下是使用装饰器来帮助快速生成接口文档,其实你这时候就会发现,如果这样写,一个接口或者属性字段得写这样一长串的装饰器,还是有点尴尬的。
这时候你可以使用一种新的方法去生成,在一个路由文件夹下新建一个
swagger.json
文件,然后在main.ts
中读取到所有的swagger.json
文件(你也可以手动一个个引入),将它们合并成一个对象,最后生成文档。这样写有一个弊端,那就是没有了提示,全靠个人熟练度🤭,不要担心,还有别的方法。
1、可以在编辑器里找插件,这样你可以在编辑器里设置一下就可以有字段提示,以及在编辑器内部打开预览(我看了一下
VSCode
里是有插件的,但是我没试过,理论上来讲可以实现)。2、再不济能下一个
Swagger Editor
这个是真量身定做的,写起来肯定嘎嘎快,而且能实时预览,写完粘贴到项目里在用第三方库生成即可。说了这么多方法,你还不写
Api
接口文档,我打洗你喔😄。
-
swagger1.json
// swagger1.json { "openapi": "3.0.0", "info": { "title": "API One", "version": "1.0.0" }, "paths": { "/api/endpoint1": { "get": { "summary": "Endpoint 1", "responses": { "200": { "description": "Successful response" } } } } } }
-
swagger2.json
// swagger2.json { "openapi": "3.0.0", "info": { "title": "API Two", "version": "1.0.0" }, "paths": { "/api/endpoint2": { "get": { "summary": "Endpoint 2", "responses": { "200": { "description": "Successful response" } } } } } }
-
集中载入生成
import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; import * as fs from 'fs'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Load and merge multiple JSON files const swaggerJson1 = JSON.parse(fs.readFileSync('swagger1.json', 'utf-8')); const swaggerJson2 = JSON.parse(fs.readFileSync('swagger2.json', 'utf-8')); const combinedSwagger = { ...swaggerJson1, ...swaggerJson2, paths: { ...swaggerJson1.paths, ...swaggerJson2.paths, }, }; // Setup Swagger options const options = new DocumentBuilder() .setTitle('Combined API') .setDescription('API documentation for multiple services') .setVersion('1.0') .build(); // Combine Swagger document and serve Swagger UI const document = SwaggerModule.createDocument(app, options, combinedSwagger); SwaggerModule.setup('api-docs', app, document); await app.listen(3000); } bootstrap();
自定义装饰器(DIY Decorator)
这个装饰器很有意思啊,这风格,不知道的我还以为我在写Java代码。在
Nestjs
里面,装饰器我觉得就是一个函数,只不过调用的表现形式不一样。装饰器可以配合Reflector
获取元数据,能够有更灵活的开发体验,你值得拥有~😎。实际上装饰器不是Nestjs特有的,而是TS的一个功能,"emitDecoratorMetadata": true, "experimentalDecorators": true
-
使用命令创建一个自定义指令文件
nest g d role
-
当然,也可以自己创建一个文件
下面就写了两个自定义的装饰器。
一个是
Role
用来用来判断权限的,和守卫那里一样,不过将SetMetadata
进行封装了一下,使用的时候直接@Role(...)
输入可通过的白名单字符串即可。第二个是
ReqUrl
,用来在Param
参数上使用的import { ExecutionContext, SetMetadata, createParamDecorator } from '@nestjs/common'; import { Request } from 'express'; // 自定义一个Role装饰器 export const Role = (...args: string[]) => SetMetadata('role', args); // 获取请求url的装饰器,这里使用的是createParamDecorator,还有其他的,如:applyDecorators能够批量处理多个装饰器。 export const ReqUrl = createParamDecorator((decorParam?: string, ctx?: ExecutionContext) => { console.log('装饰器传入的值:', decorParam); const url = ctx.switchToHttp().getRequest<Request>().url; console.log('请求的url:', url); return { url, decorParam, }; });
-
使用自定义装饰器
import { Controller, Get, UseGuards } from '@nestjs/common'; import { LoginService } from './login.service'; import { RoleGuard } from 'src/role/role.guard'; import { ReqUrl, Role } from 'src/common/decorators'; @Controller('login') export class LoginController { constructor(private readonly loginService: LoginService) {} @Get('all') @UseGuards(RoleGuard) // 使用守卫 @Role('superAdmin', 'admin') // 定义白名单 findAll(@ReqUrl('JK') url: string) { // 请求路径的获取与传值 console.log(url); // 输出ReqUrl函数u的返回值 return this.loginService.findAll(); } }
连接数据库(Database)
数据库有:
MongoDB
、MySQL
、PostgreSQL
,这三个是我用过的,其中MongoDB
是NoSQL
的数据库,即非关系型数据库,而MySQL
和PostgreSQL
则是关系型数据库。这里我将使用PostgreSQL
。
ORM
工具:typeorm
和prisma
两个,typeorm
与Nestjs
集成度最高,生态也十分不错,但是我看乎uu们都说这个ORM
工具坑很多,而且没有prisma
好用,这里我暂时保留意见,因为我没深入了解使用过。关于数据库的本地环境的安装,这里跳过。
-
引入依赖
这里的
pg
是PostgreSQL
的驱动,配合ORM
工具能够实现ts/js
代码操作数据库。dotenv
是本地环境变量的一个第三方库,用于将项目下的.env
文件里的键值对注入到本地环境变量中。pnpm install --save @nestjs/typeorm typeorm pg dotenv
-
创建环境变量文件,在项目的根目录下创建
.env
文件NODE_ENV=development HOST=127.0.0.1 PORT=5432 DB_USERNAME=postgres DB_PASSWORD=admin DB_DATABASE_NAME=postgres
-
使用
dotenv
(main.ts)这里需要在最顶部执行这个
dotenv.config()
,这样本地的.env
文件能服务启动前就注入完毕,通过process.env
就可以拿到值了。import * as dotenv from 'dotenv'; dotenv.config(); import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); await app.listen(3000); } bootstrap();
-
在项目中导入
TypeOrm
个人认为,这个
TypeOrmModule.forRoot
的里面的配置项,可以单独提取出来,创建一个config
文件,然后进行导出使用,这样会更友好一点。import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { LoginModule } from './login/login.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { resolve } from 'path'; @Module({ imports: [LoginModule, TypeOrmModule.forRoot({ type: 'postgres', // 连接的数据库 host: process.env.HOST, // 地址 port: process.env.PORT, // 端口 username: process.env.DB_USERNAME, // 数据库使用者名称 password: process.env.DB_PASSWORD, // 数据库密码 database: process.env.DB_DATABASE_NAME, // 数据库名称 entities: [resolve(__dirname, '/**/*.entity{.ts,.js}')], // 将所有的实体类注册生成表 synchronize: true, // 是否自动将实体类同步到数据库,生成环境下不建议使用 retryDelay: 500, // 重试连接数据库的间隔 retryAttempts: 1, // 重连的次数 autoLoadEntities: true, // 自动加载实体,forFeature()方法注册的每个实体都将自动添加到配置对象的实体类中 })], controllers: [AppController], providers: [AppService], }) export class AppModule {}
-
Login实体类(login.entity.ts)
相关的装饰器使用就不多赘述了,请移步查看文档了解更多。
import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity() // 使用这个装饰器,表示这是一个实体类,需要被生成一个数据表 export class Login { @PrimaryGeneratedColumn('uuid') id: number; @IsEmail() @IsNotEmpty() @Column() email: string; @IsString() @Column() password: string; }
-
导入实体类(login.module.ts)
完成这一步后,数据库会同步出一个
login
表。import { Module } from '@nestjs/common'; import { LoginService } from './login.service'; import { LoginController } from './login.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Login } from './entities/login.entity'; @Module({ imports: [TypeOrmModule.forFeature([Login])], // 导入实体类 controllers: [LoginController], providers: [LoginService], }) export class LoginModule {}
数据库的事务(Transcation)
为什么需要用到事务处理?
比如:张三要给李四转500刀,张三先要扣除500,李四才能增加500。注意这里是有一个绑定关系的,张三要是因为服务器不稳定导致扣款失败,李四是不能增加500的,同理,李四没能增加500,张三也不能扣除500。
简述:成功都成功,失败都失败。
如果处理数据库的多条行为之间没有强关联性,也可不用开启事务。这里只介绍事务的简单使用。话不多说,直接上代码!
-
快速创建一个
user
的CRUDnest g res user
-
定义创建用户的字段类型(create-user.dto.ts)
这里使用到了第三方库
class-validator
,这个第三方库里定义了大量的装饰器用于类型判断,之前有讲到过。这个文件用来定义创建用户时需要哪些参数和参数类型。import { IsNotEmpty, IsUUID, IsString } from 'class-validator'; export class CreateUserDto { @IsUUID() @IsNotEmpty() id: string; @IsString() username: string; @IsString() password: string; }
-
定义数据库映射实体(user.entity.ts)
实体文件是
ORM
工具通过数据库驱动对本地数据库生成表的,也就是说它里面的一些字段、类型等等定义都将和数据库表有一致性的。import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @Entity('User') // 创建一个表,表名为User,如果不填名称,默认就是user export class User { @PrimaryGeneratedColumn('uuid') // 主键列,uuid类型,主键不能重复,测试需注意 id: string; @Column({ comment: '用户名', // 创建一个名为username的列,列的描述为:用户名 }) username: string; @Column() password: string; }
-
在
controller
中去调用service
的方法(user.contriller.ts)controller
是相当于路由层,用于转发数据的。而service
则就是操作数据的,为controller
提供服务。import { Controller, Post, Body } from '@nestjs/common'; import { UserService } from './user.service'; import { CreateUserDto } from './dto/create-user.dto'; @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} @Post() async create(@Body() createUserDto: CreateUserDto) { // 在这里调用创建用户的方法,接下来就需要去service中去实现具体的操作 this.userService.create(createUserDto); } }
-
具体的事务实现(user.service.ts)
你可以使用
ApiFox
进行发请求,测试开启了事务和未开启事务它们会有什么不同的效果。我既然已经测试过了,肯定不会让学习这么麻烦,直接上测试请求的代码!
以下代码你可以:
- 打开浏览器
- F12打开控制台
- 将以下发请求的代码粘贴到控制台中
- 回车(前提是你的
服务端允许跨域
)
如果删除主动报错的那行代码,那么
User
表中的username
字段列的所有ikun
都将会变成GGBond
。var myHeaders = new Headers(); myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)"); myHeaders.append("Accept", "*/*"); myHeaders.append("Host", "localhost:3000"); myHeaders.append("Connection", "keep-alive"); myHeaders.append("Content-Type", "application/x-www-form-urlencoded"); var urlencoded = new URLSearchParams(); urlencoded.append("username", "ikun"); urlencoded.append("password", "I like JK"); urlencoded.append("id", "5a597c26-5238-45ac-9d89-d96ebe99158a"); var requestOptions = { method: 'POST', headers: myHeaders, body: urlencoded, redirect: 'follow' }; fetch("http://localhost:3000/user", requestOptions) .then(response => response.text()) .then(result => console.log(result)) .catch(error => console.log('error', error));
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { EntityManager, Repository } from 'typeorm'; @Injectable() export class UserService { constructor(@InjectRepository(User) private readonly userManager: Repository<User>) {} async create(createUserDto: CreateUserDto) { // 未开启事务的代码 try{ await this.userManager.save(createUserDto); throw new HttpException('事务失败测试', HttpStatus.INTERNAL_SERVER_ERROR); await this.userManager.update( { username: 'ikun', }, { username: 'GGBond', } ); }catch(e){} // 开启事务的代码 // try{ // await this.userManager.manager.transaction(async (manager: EntityManager) => { // await manager.save(User, createUserDto); // throw new HttpException('事务失败测试', HttpStatus.INTERNAL_SERVER_ERROR); // const res = await manager.update( // User, // { // username: 'ikun', // }, // { // username: 'GGBond', // } // ); // console.log(res, 'res'); // }); // }catch(e){} } }