Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !
Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。
本文基于nest8演示
基础 创建项目
nest new project-name 创建一个项目
$ tree . ├── README.md ├── nest-cli.json ├── package.json ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json 2 directories, 12 files
以下是这些核心文件的简要概述 :
app.controller.ts 带有单个路由的基本控制器示例。
app.module.ts 应用程序的根模块。
main.ts 应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
main.ts 包含一个异步函数,它负责引导我们的应用程序:
import { NestFactory } from '@nestjs/core' ;import { ApplicationModule } from './app.module' ; async function bootstrap ( ) { const app = await NestFactory .create (ApplicationModule ); await app.listen (3000 ); } bootstrap ();
NestFactory 暴露了一些静态方法用于创建应用实例
create() 方法返回一个实现 INestApplication 接口的对象, 并提供一组可用的方法
nest有两个支持开箱即用的 HTTP 平台:express 和 fastify。 您可以选择最适合您需求的产品
platform-express Express 是一个众所周知的 node.js 简约 Web 框架。 这是一个经过实战考验,适用于生产的库,拥有大量社区资源。 默认情况下使用 @nestjs/platform-express 包。 许多用户都可以使用 Express ,并且无需采取任何操作即可启用它。
platform-fastify Fastify 是一个高性能,低开销的框架,专注于提供最高的效率和速度。
Nest控制器 Nest中的控制器层负责处理传入的请求, 并返回对客户端的响应。
控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作
通过NestCLi创建控制器:
nest -h 可以看到nest支持的命令
常用命令:
创建控制器:nest g co user module
创建服务:nest g s user module
创建模块:nest g mo user module
默认以src为根路径生成
表示创建posts的控制器,这个时候会在src目录下面生成一个posts的文件夹,这个里面就是posts的控制器,代码如下
import { Controller } from '@nestjs/common' ;@Controller ('posts' ) export class PostsController {}
创建好控制器后,nestjs会自动的在 app.module.ts 中引入PostsController,代码如下
import { Module } from '@nestjs/common' ;import { AppController } from './app.controller' ;import { AppService } from './app.service' ;import { PostsController } from './posts/posts.controller' @Module ({ imports : [], controllers : [AppController , PostsController ], providers : [AppService ], }) export class AppModule {}
nest配置路由请求数据
Nestjs提供了其他HTTP请求方法的装饰器 @Get() @Post() @Put() 、 @Delete()、 @Patch()、 @Options()、 @Head()和 @All()
在Nestjs中获取Get传值或者Post提交的数据的话我们可以使用Nestjs中的装饰器来获取。
@Request() req @Response() res @Next() next @Session() req.session @Param(key?: string) req.params / req.params[key] @Body(key?: string) req.body / req.body[key] @Query(key?: string) req.query / req.query[key] @Headers(name?: string) req.headers / req.headers[name]
示例
@Controller ('posts' ) export class PostsController { constructor (private readonly postsService: PostsService ) {} @Post ('create' ) create (@Body() createPostDto: CreatePostDto ) { return this .postsService .create (createPostDto); } @Get ('list' ) findAll (@Query() query ) { return this .postsService .findAll (query); } @Get (':id' ) findById (@Param('id' ) id: string ) { return this .postsService .findById (id); } @Put (':id' ) update ( @Param('id' ) id: string, @Body() updatePostDto: UpdatePostDto, ) { return this .postsService .update (id, updatePostDto); } @Delete (':id' ) remove (@Param('id' ) id: string ) { return this .postsService .remove (id); } }
注意
关于nest的return: 当请求处理程序返回 JavaScript 对象或数组时,它将自动序列化为 JSON。但是,当它返回一个字符串时,Nest 将只发送一个字符串而不是序列化它
Nest服务
Nestjs中的服务可以是service 也可以是provider。他们都可以通过 constructor 注入依赖关系。服务本质上就是通过@Injectable() 装饰器注解的类。在Nestjs中服务相当于MVC的Model
创建服务
创建好服务后就可以在服务中定义对应的方法
import { HttpException , HttpStatus , Injectable } from '@nestjs/common' ;import { InjectRepository } from '@nestjs/typeorm' ;import { Repository , Not , Between , Equal , Like , In } from 'typeorm' ;import * as dayjs from 'dayjs' ;import { CreatePostDto } from './dto/create-post.dto' ;import { UpdatePostDto } from './dto/update-post.dto' ;import { PostsEntity } from './entities/post.entity' ;import { PostsRo } from './interfaces/posts.interface' ;@Injectable () export class PostsService { constructor ( @InjectRepository(PostsEntity) private readonly postsRepository: Repository<PostsEntity>, ) {} async create (post: CreatePostDto ) { const { title } = post; const doc = await this .postsRepository .findOne ({ where : { title } }); console .log ('doc' , doc); if (doc) { throw new HttpException ('文章标题已存在' , HttpStatus .BAD_REQUEST ); } return { data : await this .postsRepository .save (post), message : '创建成功' , }; } async findAll (query = {} as any ) { let { pageSize, pageNum, orderBy, sort, ...params } = query; orderBy = query.orderBy || 'create_time' ; sort = query.sort || 'DESC' ; pageSize = Number (query.pageSize || 10 ); pageNum = Number (query.pageNum || 1 ); console .log ('query' , query); const queryParams = {} as any; Object .keys (params).forEach ((key ) => { if (params[key]) { queryParams[key] = Like (`%${params[key]} %` ); } }); const qb = await this .postsRepository .createQueryBuilder ('post' ); qb.where (queryParams); qb.orderBy (`post.${orderBy} ` , sort); qb.skip (pageSize * (pageNum - 1 )); qb.take (pageSize); return { list : await qb.getMany (), totalNum : await qb.getCount (), total : await this .postsRepository .count (), pageSize, pageNum, }; } async findById (id : string): Promise <PostsEntity > { return await this .postsRepository .findOne ({ where : { id } }); } async update (id: string, updatePostDto: UpdatePostDto ) { const existRecord = await this .postsRepository .findOne ({ where : { id } }); if (!existRecord) { throw new HttpException (`id为${id} 的文章不存在` , HttpStatus .BAD_REQUEST ); } const updatePost = this .postsRepository .merge (existRecord, { ...updatePostDto, update_time : dayjs ().format ('YYYY-MM-DD HH:mm:ss' ), }); return { data : await this .postsRepository .save (updatePost), message : '更新成功' , }; } async remove (id: string ) { const existPost = await this .postsRepository .findOne ({ where : { id } }); if (!existPost) { throw new HttpException (`文章ID ${id} 不存在` , HttpStatus .BAD_REQUEST ); } await this .postsRepository .remove (existPost); return { data : { id }, message : '删除成功' , }; } }
Nest模块
模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构
每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时,但是对于大型程序来说这是没有意义的。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能。
@module() 装饰器接受一个描述模块属性的对象:
providers 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
controllers 必须创建的一组控制器
imports 导入模块的列表,这些模块导出了此模块中所需提供者
exports 由本模块提供并应在其他模块中可用的提供者的子集
Nestjs中的共享模块
每个模块都是一个共享模块。一旦创建就能被任意模块重复使用。假设我们将在几个模块之间共享 PostsService 实例。 我们需要把 PostsService 放到 exports 数组中:
import { Module } from '@nestjs/common' ;import { PostsController } from './posts.controller' ;import { PostsService } from './posts.service' ;@Module ({ controllers : [PostsController ], providers : [PostsService ], exports : [PostsService ] }) export class PostsModule {}
可以使用 nest g res posts 一键创建以上需要的各个模块
配置静态资源 NestJS中配置静态资源目录完整代码
npm i @nestjs/platform-express -S
import { NestExpressApplication } from '@nestjs/platform-express' ;async function bootstrap ( ) { const app = await NestFactory .create <NestExpressApplication >(AppModule ); app.useStaticAssets ('public' ) app.useStaticAssets (join (__dirname, '../public' ), { prefix : '/static/' , }); const PORT = process.env .PORT || 9000 ; await app.listen (PORT , () => Logger .log (`服务已经启动 http://localhost:${PORT} ` ), ); } bootstrap ();
配置模板引擎
配置模板引擎
import { NestFactory } from '@nestjs/core' ;import { AppModule } from './app.module' ;import {join} from 'path' ;async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); app.setBaseViewsDir (join (__dirname, '..' , 'views' )) app.setViewEngine ('ejs' ); await app.listen (9000 ); } bootstrap ();
项目根目录新建views目录然后新建根目录 -> views -> default -> index.ejs
<!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <h3 > 模板引擎</h3 > <%=message%> </body > </html >
渲染页面
Nestjs中 Render装饰器可以渲染模板,使用路由匹配渲染引擎
mport { Controller , Get , Render } from '@nestjs/common' ; import { AppService } from './app.service' ;@Controller () export class AppController { @Get () @Render ('default/index' ) getUser (): any { return {message : "hello word" } } }
Cookie的使用
cookie和session的使用依赖 于当前使用的平台,如:express和fastify 两种的使用方式不同,这里主要记录基于express 平台的用法
cookie可以用来存储用户信息,存储购物车等信息,在实际项目中用的非常多
npm instlal cookie-parser --save npm i -D @types/cookie-parser --save
引入注册
import { AppModule } from './app.module' ;import { NestExpressApplication } from '@nestjs/platform-express' ;import * as cookieParser from 'cookie-parser' async function bootstrap ( ) { const app = await NestFactory .create <NestExpressApplication >(AppModule ); app.use (cookieParser ('dafgafa' )); await app.listen (3000 ); } bootstrap ();
接口中设置cookie 使用response
请求该接口,响应一个cookie
@Get () index (@Response() res ){ res.cookie ('username' , 'poetry' , {maxAge : 1000 * 60 * 10 , httpOnly : true , signed :true }) res.send ({xxx}) }
cookie相关配置参数
domain String 指定域名下有效
expires Date 过期时间(秒),设置在某个时间点后会在该cookoe后失效
httpOnly Boolean 默认为false 如果为true表示不允许客户端(通过js来获取cookie)
maxAge String 最大失效时间(毫秒),设置在多少时间后失效
path String 表示cookie影响到的路径,如:path=/如果路径不能匹配的时候,浏览器则不发送这个cookie
secure Boolean 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效
signed Boolean 表示是否签名cookie,如果设置为true的时候表示对这个cookie签名了,这样就需要用res.signedCookies()获取值cookie不是使用res.cookies()了
获取cookie
@Get () index (@Request() req ){ console .log (req.cookies .username ) console .log (req.signedCookies .username ) return req.cookies .username }
Cookie加密
app.use (cookieParser ('123456' )); res.cookie ('userinfo' ,'hahaha' ,{domain :'.ccc.com' ,maxAge :900000 ,httpOnly :true ,signed :true }); console .log (req.signedCookies );
Session的使用
session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而session保存在服务器上
当浏览器访问服务器并发送第一次请求时,服务器端会创建一个session对象,生成一个类似于key,value的键值对, 然后将key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带key(cookie),找到对应的session(value)。 客户的信息都保存在session中
安装 express-session
npm i express-session --save npm i -D @types/express-session --save
import { AppModule } from './app.module' ;import { NestExpressApplication } from '@nestjs/platform-express' ;import * as session from 'express-seesion' async function bootstrap ( ) { const app = await NestFactory .create <NestExpressApplication >(AppModule ); app.use (session ({ secret : 'dmyxs' , cookie : { maxAge : 10000 , httpOnly : true }, rolling : true })) await app.listen (3000 ); } bootstrap ();
session相关配置参数
secret String 生成session签名的密钥
name String 客户端的cookie的名称,默认为connect.sid, 可自己设置
resave Boolean 强制保存 session 即使它并没有变化, 默认为true, 建议设置成false
saveUninitalized Boolean 强制将未初始化的 session 存储。当新建了一个 session 且未设定属性或值时,它就处于 未初始化状态。在设定一个 cookie 前,这对于登陆验证,减轻服务端存储压力,权限控制是有帮助的。默认:true, 建议手动添加
cookie Object 设置返回到前端cookie属性,默认值为{ path: ‘/’, httpOnly: true, secure: false, maxAge: null }。
rolling Boolean 在每次请求时强行设置 cookie,这将重置 cookie 过期时间, 默认为false
接口中设置session
@Get () index (@Request() req ){ req.session .username = 'poetry' }
获取session
@Get ('/session' ) session (@Request() req, @Session() session ){ console .log (req.session .username ) console .log (session.username ) return 'hello session' }
跨域,前缀路径、网站安全、请求限速 跨域,路径前缀,网络安全
import { NestFactory } from '@nestjs/core' ;import { Logger , ValidationPipe } from '@nestjs/common' ;import * as helmet from 'helmet' ;import * as csurf from 'csurf' ;import { AppModule } from './app.module' ;const PORT = process.env .PORT || 8000 ;async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); app.setGlobalPrefix ('api/v1' ); app.enableCors (); app.use (helmet ()); app.use (csurf ()); await app.listen (PORT , () => { Logger .log ( `服务已经启动,接口请访问:localhost:${PORT} ${PREFIX} ` , ) }); } bootstrap ();
限速:限制客户端在一定时间内的请求次数
yarn add @nestjs/throttler
在需要使用的模块引入使用,这里是全局 使用,在app.module.ts中引入。这里设置的是:1分钟内只能请求10次,超过则报status为429的错误
app.module .ts import { APP_GUARD } from '@nestjs/core' ;import { Module } from '@nestjs/common' ;import { UserModule } from './modules/user/user.module' ;import { ThrottlerModule , ThrottlerGuard } from '@nestjs/throttler' ;@Module ({ imports : [ UserModule , ThrottlerModule .forRoot ({ ttl : 60 , limit : 10 , }), ], providers : [ { provide : APP_GUARD , useClass : ThrottlerGuard , }, ], }) export class AppModule { }
管道、守卫、拦截器、过滤器、中间件
管道 :数据处理与转换,数据验证
守卫 :验证用户登陆,保护路由
拦截器 :对请求响应进行拦截,统一响应内容
过滤器 :异常捕获
中间件 :日志打印
执行顺序(时机)
从客户端发送一个post请求,路径为:/user/login,请求参数为:{userinfo: ‘xx’,password: ‘xx’},到服务器接收请求内容,触发绑定的函数并且执行相关逻辑完毕,然后返回内容给客户端的整个过程大体上要经过如下几个步骤:
全局使用: 管道 - 守卫 - 拦截器 - 过滤器 - 中间件。统一在main.ts文件中使用,全局生效
import { NestFactory } from '@nestjs/core' ;import { ParseIntPipe } from '@nestjs/common' ;import { AppModule } from './app.module' ;import { HttpExceptionFilter } from './common/filters/http-exception.filter' ;import { LoggerMiddleware } from './common/middleware/logger.middleware' ;import { AuthGuard } from './common/guard/auth.guard' ;import { AuthInterceptor } from './common/interceptors/auth.interceptor' ;async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); app.useGlobalPipes (new ParseIntPipe ()); app.use (LoggerMiddleware ) app.useGlobalFilters (new HttpExceptionFilter ()); app.useGlobalGuards (new AuthGuard ()); app.useGlobalInterceptors (new AuthInterceptor ()); await app.listen (3000 ); } bootstrap ();
管道 常用内置管道,从@nestjs/common导出
ParseIntPipe:将字符串数字转数字
ValidationPipe:验证管道
局部使用管道
匹配整个路径,使用UsePipes
只匹配某个接口,使用UsePipes
在获取参数时匹配,一般使用内置管道
import { Controller , Get , Put , Body , Param , UsePipes , ParseIntPipe } from '@nestjs/common' ; import { myPipe } from '../../common/pipes/user.pipe' ;@Controller ('user' ) @UsePipes (new myPipe ()) export class UserController { @Get (':id' ) getUserById (@Param('id' , new ParseIntPipe()) id ) { console .log ('user' , typeof id); return id; } @Put (':id' ) @UsePipes (new myPipe ()) updateUser (@Body() user, @Param('id' ) id ) { return { user, id, }; } }
自定义管道
使用快捷命令生成:nest g pi myPipe common/pipes
import { ArgumentMetadata , Injectable , PipeTransform , BadRequestException , } from '@nestjs/common' ; @Injectable () export class myPipe implements PipeTransform <string> { transform (value: string, metadata: ArgumentMetadata ) { if (metadata.type === 'body' ) { console .log ('来自请求体' , value); } if (metadata.type === 'param' ) { console .log ('来自查询路径' , value); const val = parseInt (value, 10 ); if (isNaN (val)) { throw new BadRequestException ('Validation failed' ); } return val; } return value; } }
守卫
自定义守卫
使用快捷命令生成:nest g gu myGuard common/guards
import { CanActivate , ExecutionContext , Injectable } from '@nestjs/common' ;import { Reflector } from '@nestjs/core' ; @Injectable () export class AuthGuard implements CanActivate { constructor (private readonly reflector: Reflector ) { } private whiteUrlList : string[] = ['/user' ]; private isWhiteUrl (urlList : string[], url : string): boolean { if (urlList.includes (url)) { return true ; } return false ; } canActivate (context : ExecutionContext ): boolean { const request = context.switchToHttp ().getRequest (); if (this .isWhiteUrl (this .whiteUrlList , request.url )) { return true ; } else { return false ; } const roles = this .reflector .get <string[]>('roles' , context.getHandler ()); const { user } = request.query ; if (roles.includes (user)) { return true ; } else { return false ; } const token = context.switchToRpc ().getData ().headers .token ; const userinfo = context.switchToHttp ().getRequest ().session ; return true ; } }
局部使用守卫
import { Controller , Get , Delete , Param , UsePipes , UseGuards , ParseIntPipe , } from '@nestjs/common' ; import { AuthGuard } from '../../common/guard/auth.guard' ;import { Role } from '../../common/decorator/role.decorator' ; @UseGuards (AuthGuard ) @Controller ('user' ) export class UserController { @Get (':id' ) getUserById (@Param('id' , new ParseIntPipe()) id ) { console .log ('user' , typeof id); return id; } @Delete (':id' ) @Role ('admin' ) removeUser (@Param('id' ) id ) { return id; } }
装饰器 自定义守卫中使用到了自定义装饰器
nest g d role common/decorator
import { SetMetadata } from '@nestjs/common' ;export const Roles = (...roles: string[] ) => SetMetadata ('roles' , roles);
拦截器 使用快捷命令生成:nest g in auth common/intercepters
import { CallHandler , ExecutionContext , Injectable , NestInterceptor , } from '@nestjs/common' ; import { map } from 'rxjs/operators' ;import { Observable } from 'rxjs' ;@Injectable () export class AuthInterceptor implements NestInterceptor { intercept (context : ExecutionContext , next : CallHandler ): Observable <any> { const request = context.switchToHttp ().getRequest (); console .log ('拦截器' , request.url ); return next.handle ().pipe ( map ((data ) => { console .log ('全局响应拦截器方法返回内容后...' ); return { status : 200 , timestamp : new Date ().toISOString (), path : request.url , message : '请求成功' , data : data, }; }), ); } }
过滤器
局部使用过滤器
import { Controller , Get , UseFilters , HttpException , HttpStatus , } from '@nestjs/common' ; import { HttpExceptionFilter } from '../../common/filters/http-exception.filter' ;@UseFilters (new HttpExceptionFilter ()) @Controller ('/user' ) export class ExceptionController { @Get () getUserById (@Query () { id }): string { if (!id) { throw new HttpException ( { status : HttpStatus .BAD_REQUEST , message : '请求参数id 必传' , error : 'id is required' , }, HttpStatus .BAD_REQUEST , ); } return 'hello error' ; } }
自定义过滤器
使用快捷命令生成:nest g f myFilter common/filters
import { ArgumentsHost , Catch , ExceptionFilter , HttpException , } from '@nestjs/common' ; @Catch (HttpException ) export class HttpExceptionFilter implements ExceptionFilter <HttpException > { catch (exception : HttpException , host : ArgumentsHost ) { const ctx = host.switchToHttp (); const response = ctx.getResponse (); const request = ctx.getRequest (); const status = exception.getStatus (); const exceptionRes : any = exception.getResponse (); const { error, message } = exceptionRes; const msgLog = { status, timestamp : new Date ().toISOString (), path : request.url , error, message, }; response.status (status).json (msgLog); } }
中间件
局部使用中间件
import { Module , MiddlewareConsumer , RequestMethod } from '@nestjs/common' ;import { LoggerMiddleware } from './common/middleware/logger.middlerware' ;import { UserModule } from './modules/user/user.module' ;@Module ({ imports :[ UserModule ] }) export class AppModule { configure (consumer: MiddlewareConsumer ) { consumer .apply (LoggerMiddleware ) .exclude ({ path : 'user' , method : RequestMethod .POST }) .forRoutes ('user' ); } }
自定义中间件
nest g mi logger common/middleware
import { Injectable , NestMiddleware } from '@nestjs/common' ;import { Request , Response } from 'express' ;@Injectable () export class LoggerMiddleware implements NestMiddleware { use (req: Request, res: Response, next: () => void ) { const { method, path } = req; console .log (`${method} ${path} ` ); next (); } }
函数式中间件
export function logger (req, res, next ) { next (); } async function bootstrap ( ) { const app = await NestFactory .create <NestExpressApplication >(AppModule ); app.use (logger); } bootstrap ();
一例看懂中间件、守卫、管道、异常过滤器、拦截器
从客户端发送一个post请求,路径为:/user/login,请求参数为:{userinfo: ‘xx’,password: ‘xx’},到服务器接收请求内容,触发绑定的函数并且执行相关逻辑完毕,然后返回内容给客户端的整个过程大体上要经过如下几个步骤:`
项目需要包支持:
npm install --save rxjs xml2js class-validator class-transformer
建立user模块:模块内容结构:
nest g res user
user.controller.ts文件
import { Controller , Post , Body } from '@nestjs/common' ; import { UserService } from './user.service' ;import { UserLoginDTO } from './dto/user.login.dto' ;@Controller ('user' ) export class UserController { constructor (private readonly userService: UserService ) {} @Post ('test' ) loginIn (@Body() userlogindto: UserLoginDTO ) { return userlogindto; } }
user.module.ts文件
import { Module } from '@nestjs/common' ;import { UserController } from './user.controller' ;import { UserService } from './user.service' ;@Module ({ controllers : [UserController ], providers : [UserService ], }) export class UserModule {}
user.service.ts文件
import { Injectable } from '@nestjs/common' ;@Injectable () export class UserService {}
user.login.dto.ts文件
import { IsNotIn , MinLength } from 'class-validator' ;export class UserLoginDTO { @IsNotIn (['' ,undefined ,null ],{message : '账号不能为空' }) username : string; @MinLength (6 ,{ message : '密码长度不能小于6位数' }) password : string; }
app.module.ts文件
import { Module } from '@nestjs/common' ;import { UserModule } from './user/user.module' @Module ({ imports : [ UserModule ] }) export class AppModule {}
新建common文件夹里面分别建立对应的文件夹以及文件: 中间件(middleware) — xml.middleware.ts 守卫(guard) — auth.guard.ts 管道(pipe) — validation.pipe.ts 异常过滤器(filters) — http-exception.filter.ts 拦截器(interceptor) — response.interceptor.ts
import { NestFactory } from '@nestjs/core' ;import { AppModule } from './app.module' ;import { ValidationPipe } from './common/pipe/validation.pipe' ;import { HttpExceptionFilter } from './common/filters/http-exception.filter' ;import { XMLMiddleware } from './common/middleware/xml.middleware' ;import { AuthGuard } from './common/guard/auth.guard' ;import { ResponseInterceptor } from './common/interceptor/response.interceptor' ;async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); app.useGlobalPipes (new ValidationPipe ()); app.useGlobalFilters (new HttpExceptionFilter ()); app.use (new XMLMiddleware ().use ); app.useGlobalGuards (new AuthGuard ()); app.useGlobalInterceptors (new ResponseInterceptor ()); await app.listen (3001 ); } bootstrap ();
中间件是请求的第一道关卡
执行任何代码。
对请求和响应对象进行更改。
结束请求-响应周期。
调用堆栈中的下一个中间件函数。
如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起
本例中:使用中间件让express支持xml请求并且将xml内容转换为json数组
import { Injectable , NestMiddleware } from '@nestjs/common' ;import { Request , Response } from 'express' ;const xml2js = require ('xml2js' );const parser = new xml2js.Parser ();@Injectable () export class XMLMiddleware implements NestMiddleware { use (req: Request, res: Response, next: Function ) { console .log ('进入全局xml中间件...' ); if (req.headers ['content-type' ] && req.headers ['content-type' ].includes ('application/xml' )){ req.on ('data' , mreq => { parser.parseString (mreq,function (err,result ){ console .log ('parseString转换后的数据' ,result); req['body' ]= result; }) }) } next (); } }
注册方式
同一路由注册多个中间件的执行顺序为,先是全局中间件执行,然后是模块中间件执行,模块中的中间件顺序按照.apply中注册的顺序执行
守卫是第二道关卡
守卫控制一些权限内容,如:一些接口需要带上token标记,才能够调用,守卫则是对这个标记进行验证操作的。 本例中代码如下:
import {Injectable ,CanActivate ,HttpException ,HttpStatus ,ExecutionContext ,} from '@nestjs/common' ;@Injectable () export class AuthGuard implements CanActivate { async canActivate (context : ExecutionContext ): Promise <boolean> { console .log ('进入全局权限守卫...' ); const request = context.switchToHttp ().getRequest (); const token = context.switchToRpc ().getData ().headers .token ; if (this .hasUrl (this .urlList , request.url )) { return true ; } if (token) { try { return true ; } catch (e) { throw new HttpException ( '没有授权访问,请先登录' , HttpStatus .UNAUTHORIZED , ); } } else { throw new HttpException ( '没有授权访问,请先登录' , HttpStatus .UNAUTHORIZED , ); } }; private urlList : string[] = [ '/user/login' ]; private hasUrl (urlList : string[], url : string): boolean { let flag : boolean = false ; if (urlList.indexOf (url) >= 0 ) { flag = true ; } return flag; } };
注册方式
全局注册:在main.ts中导入需要的守卫模块如:AuthGuard。然后使用 app.useGlobalGuards(new AuthGuard()) 即可
模块注册:在需要注册的controller控制器中导入AuthGuard。然后从@nestjs/common中导UseGuards装饰器。最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可
同一路由注册多个守卫的执行顺序为,先是全局守卫执行,然后是模块中守卫执行
拦截器是第三道关卡 想到自定义返回内容如
{ "statusCode": 400, "timestamp": "2022-05-14T08:06:45.265Z", "path": "/user/login", "message": "请求失败", "data": { "isNotIn": "账号不能为空" } }
这个时候就可以使用拦截器来做一下处理了。拦截器作用:
在函数执行之前/之后绑定额外的逻辑
转换从函数返回的结果
转换从函数抛出的异常
扩展基本函数行为
根据所选条件完全重写函数 (例如, 缓存目的)
拦截器的执行顺序分为两个部分:
第一个部分在管道和自定义逻辑(next.handle()方法)之前。
第二个部分在管道和自定义逻辑(next.handle()方法)之后。
import { Injectable , NestInterceptor , CallHandler , ExecutionContext , } from '@nestjs/common' ; import { map } from 'rxjs/operators' ;import { Observable } from 'rxjs' ;interface Response <T> { data : T; } @Injectable () export class ResponseInterceptor <T> implements NestInterceptor <T, Response <T>> { intercept ( context : ExecutionContext , next : CallHandler <T>, ): Observable <Response <T>> { const ctx = context.switchToHttp (); const request = ctx.getRequest (); console .log ('进入全局响应拦截器...' ); return next.handle ().pipe ( map (data => { console .log ('全局响应拦截器方法返回内容后...' ); return { statusCode : 0 , timestamp : new Date ().toISOString (), path : request.url , message : '请求成功' , data :data }; }), ); } }
中间多了个全局管道以及自定义逻辑,即只有路由绑定的函数有正确的返回值之后才会有next.handle()之后的内容
注册方式
全局注册:在main.ts中导入需要的模块如:ResponseInterceptor。然后使用 app.useGlobalInterceptors(new ResponseInterceptor()) 即可
模块注册:在需要注册的controller控制器中导入ResponseInterceptor。然后从@nestjs/common中导入UseInterceptors装饰器。最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可
同一路由注册多个拦截器时候,优先执行模块中绑定的拦截器,然后其拦截器转换的内容将作为全局拦截器的内容,即包裹两次返回内容如:
{ // 全局拦截器效果 "statusCode": 0, "timestamp": "2022-05-14T08:20:06.159Z", "path": "/user/login", "message": "请求成功", "data": { "pagenum": 1, // 模块中拦截器包裹效果 “pageSize": 10 "list": [] } }
管道是第四道关卡
认识官方的三个内置管道
ValidationPipe:基于class-validator和class-transformer这两个npm包编写的一个常规的验证管道,可以从class-validator导入配置规则,然后直接使用验证(当前不需要了解ValidationPipe的原理,只需要知道从class-validator引规则,设定到对应字段,然后使用ValidationPipe即可)
ParseIntPipe:转换传入的参数为数字
如:传递过来的是/test?id=‘123’”这里会将字符串‘123’转换成数字123 3. ParseUUIDPipe :验证字符串是否是 UUID(通用唯一识别码)
如:传递过来的是/test?id=‘xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’”这里会验证格式是否正确,不正确则抛出错误,否则调用findOne方法
本例中管道使用如下:
import { validate } from 'class-validator' ;import { plainToClass } from 'class-transformer' ;import { PipeTransform , Injectable , ArgumentMetadata , BadRequestException } from '@nestjs/common' ;@Injectable () export class ValidationPipe implements PipeTransform <any>{ async transform (value: any, { metatype }: ArgumentMetadata ) { console .log ('进入全局管道...' ); if (!metatype || !this .toValidate (metatype)) { return value; } const object = plainToClass (metatype, value); const errors = await validate (object); if (errors.length > 0 ) { let errormsg = errors.shift ().constraints ; throw new BadRequestException (errormsg); } return value; } private toValidate (metatype : any): boolean { const types : Function [] = [String , Boolean , Number , Array , Object ]; return !types.includes (metatype); } }
注册方式
全局注册:在main.ts中导入需要的模块如:ValidationPipe;然后使用 app.useGlobalPipes(new ValidationPipe()) 即可
模块注册:在需要注册的controller控制器中导入ValidationPipe;然后从@nestjs/common中导入UsePipes装饰器;最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可,管道还允许注册在相关的参数上如:@Body/@Query… 等
**注意:**同一路由注册多个管道的时候,优先执行全局管道,然后再执行模块管道:
异常过滤器是所有抛出的异常的统一处理方案
简单来讲就是捕获系统抛出的所有异常,然后自定义修改异常内容,抛出友好的提示。
内置异常类
系统提供了不少内置的系统异常类,需要的时候直接使用throw new XXX(描述,状态)这样的方式即可抛出对应的异常,一旦抛出异常,当前请求将会终止。
注意每个异常抛出的状态码有所不同 。如:
BadRequestException — 400 UnauthorizedException — 401 ForbiddenException — 403 NotFoundException — 404 NotAcceptableException — 406 RequestTimeoutException — 408 ConflictException — 409 GoneException — 410 PayloadTooLargeException — 413 UnsupportedMediaTypeException — 415 UnprocessableEntityException — 422 InternalServerErrorException — 500 NotImplementedException — 501 BadGatewayException — 502 ServiceUnavailableException — 503 GatewayTimeoutException — 504
本例中使用的是自定义的异常类,代码如下:
import { ExceptionFilter , Catch , ArgumentsHost , HttpException ,Logger ,HttpStatus } from '@nestjs/common' ;import { Request , Response } from 'express' ;@Catch () export class HttpExceptionFilter implements ExceptionFilter { catch (exception : HttpException , host : ArgumentsHost ) { console .log ('进入全局异常过滤器...' ); const ctx = host.switchToHttp (); const response = ctx.getResponse <Response >(); const request = ctx.getRequest <Request >(); const status = exception instanceof HttpException ? exception.getStatus () : HttpStatus .INTERNAL_SERVER_ERROR ; const message = exception.message || exception.message .message || exception.message .error || null ; let msgLog = { statusCode : status, timestamp : new Date ().toISOString (), path : request.url , message : '请求失败' , data : message } Logger .error ( '错误信息' , JSON .stringify (msgLog), 'HttpExceptionFilter' , ); response .status (status) .json (msgLog); } }
注册方式
全局注册:在main.ts中导入需要的模块如:HttpExceptionFilter 然后使用 app.useGlobalFilters(new HttpExceptionFilter()) 即可
模块注册:在需要注册的controller控制器中导入HttpExceptionFilter然后从@nestjs/common中导入UseFilters装饰器;最后直接放置在对应的@Controller()或者@Post/@Get…等装饰器之下即可
注意: 同一路由注册多个管道的时候,只会执行一个异常过滤器,优先执行模块中绑定的异常过滤器,如果模块中无绑定异常过滤则执行全局异常过滤器
数据验证 如何 限制 和 验证 前端传递过来的数据?
常用:dto(data transfer object数据传输对象) + class-validator,自定义提示内容,还能集成swagger
class-validator的验证项装饰器
https://github.com/typestack/class-validator#usage
@IsOptional() //可选的 @IsNotEmpty({ message: ‘不能为空’ }) @MinLength(6, {message: ‘密码长度不能小于6位’}) @MaxLength(20, {message: ‘密码长度不能超过20位’})
@IsEmail({}, { message: ‘邮箱格式错误’ }) //邮箱 @IsMobilePhone(‘zh-CN’, {}, { message: ‘手机号码格式错误’ }) //手机号码 @IsEnum([0, 1], {message: ‘只能传入数字0或1’}) //枚举
@ValidateIf(o => o.username === ‘admin’) //条件判断,条件满足才验证,如:这里是传入的username是admin才验证
yarn add class-validator class-transformer
全局使用内置管道ValidationPipe ,不然会报错,无法起作用
import { NestFactory } from '@nestjs/core' ;import { Logger , ValidationPipe } from '@nestjs/common' ;import { AppModule } from './app.module' ;async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); app.useGlobalPipes (new ValidationPipe ()); await app.listen (3000 ); } bootstrap ();
编写dto,使用class-validator的校验项验证
创建DTO:只需要用户名,密码即可,两种都不能为空
可以使用nest g res user一键创建带有dto的接口模块
import { IsNotEmpty , MinLength , MaxLength } from 'class-validator' ;export class CreateUserDto { @IsNotEmpty ({ message : '用户名不能为空' }) username : string; @IsNotEmpty ({ message : '密码不能为空' }) @MinLength (6 , { message : '密码长度不能小于6位' , }) @MaxLength (20 , { message : '密码长度不能超过20位' , }) password : string; }
修改DTO:用户名,密码,手机号码,邮箱,性别,状态,都是可选的
import { IsEnum , MinLength , MaxLength , IsOptional , IsEmail , IsMobilePhone , } from 'class-validator' ; import { Type } from 'class-transformer' ;export class UpdateUserDto { @IsOptional () username : string; @IsOptional () @MinLength (6 , { message : '密码长度不能小于6位' , }) @MaxLength (20 , { message : '密码长度不能超过20位' , }) password : string; @IsOptional () @IsEmail ({}, { message : '邮箱格式错误' }) email : string; @IsOptional () @IsMobilePhone ('zh-CN' , {}, { message : '手机号码格式错误' }) mobile : string; @IsOptional () @IsEnum (['male' , 'female' ], { message : 'gender只能传入字符串male或female' , }) gender : string; @IsOptional () @IsEnum ({ 禁用: 0 , 可用: 1 },{ message : 'status只能传入数字0或1' , }) @Type (() => Number ) status : number; }
controller和service一起使用
import { Controller , Post , Body , HttpCode , HttpStatus , } 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 () @HttpCode (HttpStatus .OK ) async create (@Body() user: CreateUserDto ) { return await this .userService .create (user); } @Patch (':id' ) async update (@Param('id' ) id: string, @Body() user: UpdateUserDto ) { return await this .userService .update (id, user); } }
import { Injectable } from '@nestjs/common' ;import { Repository } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { ToolsService } from '../../utils/tools.service' ;import { CreateUserDto } from './dto/create-user.dto' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async create (user: CreateUserDto ) { do some thing.... } }
进阶 配置抽离
app.module .ts import * as path from 'path' ;import { Module } from '@nestjs/common' ;import { TypeOrmModule } from '@nestjs/typeorm' ;import { ConfigModule , ConfigService } from 'nestjs-config' ;@Module ({ imports : [ ConfigModule .load (path.resolve (__dirname, 'config' , '**/!(*.d).{ts,js}' )), TypeOrmModule .forRootAsync ({ useFactory : (config: ConfigService ) => config.get ('database' ), inject : [ConfigService ], }) ], controllers : [AppController ], providers : [AppService ], }) export class AppModule {}
配置数据库
src -> config -> database import { join } from 'path' ;export default { type : 'mysql' , host : 'localhost' , port : 3306 , username : 'root' , password : 'your password' , database : 'test' , entities : [join (__dirname, '../' , '**/**.entity{.ts,.js}' )], synchronize : true , };
环境配置
cross-env的作用是兼容window系统和mac系统来设置环境变量
在package.json中配置
"scripts": { "start:dev": "cross-env NODE_ENV=development nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "cross-env NODE_ENV=production node dist/main", },
dotenv的使用
根目录创建 env.parse.ts
import * as fs from 'fs' ;import * as path from 'path' ;import * as dotenv from 'dotenv' ;const isProd = process.env .NODE_ENV === 'production' ;const localEnv = path.resolve ('.env.local' );const prodEnv = path.resolve ('.env.prod' );const filePath = isProd && fs.existsSync (prodEnv) ? prodEnv : localEnv;dotenv.config ({ path : filePath });
导入环境
// main.ts import '../env.parse'; // 导入环境变量
.env.local
PORT=9000 MYSQL_HOST=127.0.0.1 MYSQL_PORT=3306 MYSQL_USER=root MYSQL_PASSWORD=123 MYSQL_DATABASE=test
.env.prod
PORT=9000 MYSQL_HOST=127.0.0.1 MYSQL_PORT=3306 MYSQL_USER=root MYSQL_PASSWORD=1234 MYSQL_DATABASE=test
读取环境变量 process.env.MYSQL_HOST形式
文件上传与下载 yarn add @nestjs/platform-express compressing compressing 文件下载依赖,提供流的方式
配置文件的目录地址,以及文件的名字格式
import { join } from 'path' ;import { diskStorage } from 'multer' ;export default { root : join (__dirname, '../../assets/uploads' ), storage : diskStorage ({ destination : join ( __dirname, `../../assets/uploads/${new Date ().toLocaleDateString()} ` , ), filename : (req, file, cb ) => { const filename = `${new Date ().getTime()} .${file.mimetype.split('/' )[1 ]} ` ; return cb (null , filename); }, }), };
import { ConfigModule , ConfigService } from 'nestjs-config' ;@Module ({ imports : [ ConfigModule .load (resolve (__dirname, 'config' , '**/!(*.d).{ts,js}' )), ], controllers : [], providers : [], }) export class AppModule implements NestModule {}
import { Controller , Get , Post , UseInterceptors , UploadedFile , UploadedFiles , Body , Res , } from '@nestjs/common' ; import { FileInterceptor , FilesInterceptor } from '@nestjs/platform-express' ;import { FileUploadDto } from './dto/upload-file.dto' ;import { UploadService } from './upload.service' ;import { Response } from 'express' ;@Controller ('common' ) export class UploadController { constructor (private readonly uploadService: UploadService ) {} @Post ('upload' ) @UseInterceptors (FileInterceptor ('file' )) uploadFile (@UploadedFile() file ) { this .uploadService .uploadSingleFile (file); return true ; } @Post ('uploads' ) @UseInterceptors (FilesInterceptor ('file' )) uploadMuliFile (@UploadedFiles() files, @Body() body ) { this .uploadService .UploadMuliFile (files, body); return true ; } @Get ('export' ) async downloadAll (@Res() res: Response ) { const { filename, tarStream } = await this .uploadService .downloadAll (); res.setHeader ('Content-Type' , 'application/octet-stream' ); res.setHeader ('Content-Disposition' , `attachment; filename=${filename} ` ); tarStream.pipe (res); } }
import { Injectable , HttpException , HttpStatus } from '@nestjs/common' ;import { join } from 'path' ;import { createWriteStream } from 'fs' ;import { tar } from 'compressing' ;import { ConfigService } from 'nestjs-config' ;@Injectable () export class UploadService { constructor (private readonly configService: ConfigService ) {} uploadSingleFile (file: any ) { console .log ('file' , file); } UploadMuliFile (files : any, body : any) { console .log ('files' , files); } async downloadAll ( ) { const uploadDir = this .configService .get ('file' ).root ; const tarStream = new tar.Stream (); await tarStream.addEntry (uploadDir); return { filename : 'download.tar' , tarStream }; } }
import { Module } from '@nestjs/common' ;import { MulterModule } from '@nestjs/platform-express' ;import { ConfigService } from 'nestjs-config' ;import { UploadService } from './upload.service' ;import { UploadController } from './upload.controller' ;@Module ({ imports : [ MulterModule .registerAsync ({ useFactory : (config: ConfigService ) => config.get ('file' ), inject : [ConfigService ], }), ], controllers : [UploadController ], providers : [UploadService ], }) export class UploadModule {}
实现图片随机验证码 nest如何实现图片随机验证码?
这里使用的是svg-captcha 这个库,你也可以使用其他的库
封装,以便多次调用
src -> utils -> tools.service .ts import { Injectable } from '@nestjs/common' ;import * as svgCaptcha from 'svg-captcha' ;@Injectable () export class ToolsService { async captche (size = 4 ) { const captcha = svgCaptcha.create ({ size, fontSize : 50 , width : 100 , height : 34 , background : '#cc9966' , }); return captcha; } }
在使用的module中引入
import { Module } from '@nestjs/common' ;import { UserController } from './user.controller' ;import { UserService } from './user.service' ;import { ToolsService } from '../../utils/tools.service' ;@Module ({ controllers : [UserController ], providers : [UserService , ToolsService ], }) export class UserModule { }
使用
import { Controller , Get , Post ,Body } from '@nestjs/common' ;import { EmailService } from './email.service' ;@Controller ('user' ) export class UserController { constructor (private readonly toolsService: ToolsService, ) {} @Get ('authcode' ) async getCode (@Req() req, @Res() res ) { const svgCaptcha = await this .toolsService .captche (); req.session .code = svgCaptcha.text ; console .log (req.session .code ); res.type ('image/svg+xml' ); res.send (svgCaptcha.data ); } @Post ('/login' ) login (@Body() body, @Session() session ) { const { code } = body; if (code?.toUpperCase () === session.code ?.toUpperCase ()){ console .log (‘验证码通过’) } return 'hello authcode' ; } }
前端简单代码
<!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > <style > form { display : flex; } .input { width : 80px ; height : 32px ; } .verify_img { margin : 0px 5px ; } </style > </head > <body > <h2 > 随机验证码</h2 > <form action ="/user/login" method ="post" enctype ="application/x-www-form-urlencoded" > <input type ="text" name ='code' class ="input" /> <img class ="verify_img" src ="/user/code" title ="看不清?点击刷新" onclick ="javascript:this.src='/user/code?t='+Math.random()" > //点击再次生成新的验证码 <button type ="submit" > 提交</button > </form > </body > </html >
邮件服务
邮件服务使用文档 https://nest-modules.github.io/mailer/docs/mailer
import { MailerModule } from '@nestjs-modules/mailer' ;import { resolve, join } from 'path' ;import { ConfigModule , ConfigService } from 'nestjs-config' ;@Module ({ imports : [ ConfigModule .load (resolve (__dirname, 'config' , '**/!(*.d).{ts,js}' )), MailerModule .forRootAsync ({ useFactory : (config: ConfigService ) => config.get ('email' ), inject : [ConfigService ], }), ], controllers : [], providers : [], }) export class AppModule implements NestModule {}import { join } from 'path' ;import { EjsAdapter } from '@nestjs-modules/mailer/dist/adapters/ejs.adapter' ;export default { transport : { host : 'smtp.qq.com' , secureConnection : true , secure : true , port : 465 , ignoreTLS : false , auth : { user : '123@test.com' , pass : 'dfafew1' , }, }, defaults : { from : '"nestjs" <123@test.com>' , }, template : { dir : join (__dirname, '../templates/email' ), adapter : new EjsAdapter (), options : { strict : true , }, }, };
邮件服务使用
import { Injectable } from '@nestjs/common' ;import { MailerService } from '@nestjs-modules/mailer' ;@Injectable () export class EmailService { constructor (private mailerService: MailerService ) {} async sendEmail ( ) { console .log ('发送邮件' ); await this .mailerService .sendMail ({ to : 'test@qq.com' , from : '123@test.com' , text : 'welcome' , html : '<h1>hello</h1>' , }); return '发送成功' ; } }
nest基于possport + jwt做登陆验证 方式与逻辑
基于possport的本地策略和jwt策略
本地策略 主要是验证账号和密码是否存在,如果存在就登陆,返回token
jwt策略 则是验证用户登陆时附带的token 是否匹配和有效,如果不匹配和无效则返回401状态码
yarn add @nestjs/jwt @nestjs/passport passport-jwt passport-local passport yarn add -D @types/passport @types/passport-jwt @types/passport-local
jwt策略 jwt.strategy.ts
import { Strategy , ExtractJwt , StrategyOptions } from 'passport-jwt' ;import { Injectable } from '@nestjs/common' ;import { PassportStrategy } from '@nestjs/passport' ;import { jwtConstants } from './constants' ;@Injectable () export class JwtStrategy extends PassportStrategy (Strategy ) { constructor ( ) { super ({ jwtFromRequest : ExtractJwt .fromHeader ('token' ), ignoreExpiration : false , secretOrKey : jwtConstants.secret , } as StrategyOptions ); } async validate (payload: any ) { return { userId : payload.userId , username : payload.username }; } }
本地策略 local.strategy.ts
import { Strategy , IStrategyOptions } from 'passport-local' ;import { Injectable , HttpException , HttpStatus } from '@nestjs/common' ;import { PassportStrategy } from '@nestjs/passport' ;import { AuthService } from './auth.service' ;@Injectable () export class LocalStrategy extends PassportStrategy (Strategy ) { constructor (private authService: AuthService ) { super ({ usernameField : 'username' , passwordField : 'password' , } as IStrategyOptions ); } async validate (username : string, password : string): Promise <any> { return await this .authService .validateUser ({ username, password }); } }
constants.ts
export const jwtConstants = { secret : 'secretKey' , };
使用守卫 auth.controller.ts
import { Controller , Get , Post , Request , UseGuards } from '@nestjs/common' ;import { AuthGuard } from '@nestjs/passport' ;import { AuthService } from './auth.service' ;@Controller ('auth' ) export class AuthController { constructor (private readonly authService: AuthService ) {} @UseGuards (AuthGuard ('local' )) @Post ('login' ) async login (@Request() req ) { return this .authService .login (req.user ); } @UseGuards (AuthGuard ('jwt' )) @Get ('userInfo' ) getUserInfo (@Request() req ) { return req.user ; } }
在module引入jwt配置和数据库查询的实体 auth.module.ts
import { LocalStrategy } from './local.strategy' ;import { jwtConstants } from './constants' ;import { Module } from '@nestjs/common' ;import { PassportModule } from '@nestjs/passport' ;import { JwtModule } from '@nestjs/jwt' ;import { AuthService } from './auth.service' ;import { AuthController } from './auth.controller' ;import { JwtStrategy } from './jwt.strategy' ;import { UsersEntity } from '../user/entities/user.entity' ;import { TypeOrmModule } from '@nestjs/typeorm' ;@Module ({ imports : [ TypeOrmModule .forFeature ([UsersEntity ]), PassportModule , JwtModule .register ({ secret : jwtConstants.secret , signOptions : { expiresIn : '10d' }, }), ], controllers : [AuthController ], providers : [AuthService , LocalStrategy , JwtStrategy ], exports : [AuthService ], }) export class AuthModule {}
auth.service.ts
import { Injectable } from '@nestjs/common' ;import { JwtService } from '@nestjs/jwt' ;import { compareSync } from 'bcryptjs' ;@Injectable () export class AuthService { constructor ( @InjectRepository(UsersEntity), private readonly usersRepository: Repository<UsersEntity>, private jwtService: JwtService ) {} validateUser (username: string, password: string ) { const user = await this .usersRepository .findOne ({ where : { username }, select : ['username' , 'password' ], }); if (!user) ToolsService .fail ('用户名或密码不正确' ); if (!compareSync (password, user.password )) { ToolsService .fail ('用户名或密码不正确' ); } return user; } login (user: any ) { const payload = { username : user.username }; return { token : this .jwtService .sign (payload), }; } }
最后在app.module.ts中导入即可测试
import { AuthModule } from './modules/auth/auth.module' ;@Module ({ imports : [ ... AuthModule , ], controllers : [AppController ], providers : [], }) export class AppModule implements NestModule {}
使用postman测试
对数据库的密码加密:md5和bcryptjs 密码加密
一般开发中,是不会有人直接将密码明文直接放到数据库当中的。因为这种做法是非常不安全的,需要对密码进行加密处理。 好处:
预防内部网站运营人员知道用户的密码
预防外部的攻击,尽可能保护用户的隐私
加密方式
使用md5:每次生成的值是一样的,一些网站可以破解,因为每次存储的都是一样的值
使用bcryptjs:每次生成的值是不一样的
加密
import * as md5 from 'md5' ;const passwrod = '123456' ;const transP = md5 (passwrod);
给密码加点”盐”:目的是混淆密码,其实还是得到固定的值
const passwrod = '123456' ;const salt = 'dmxys' const transP = md5 (passwrod + salt);
验证密码:先加密,再验证
const passwrod = '123456' ;const databasePassword = 'e10adc3949ba59abbe56e057f20f883e' if (md5 (passwrod) === databasePassword ) { console .log ('密码通过' ); }
使用bcryptjs
yarn add bcryptjs yarn add -D @types/bcryptjs
同一密码,每次生成不一样的值
import { compareSync, hashSync } from 'bcryptjs' ;const passwrod = '123456' ;const transformPass = hashSync (passwrod); $2a$10$HgTA1GX8uxbocSQlbQ42/.Y2XnIL7FyfKzn6IC69IXveD6F9LiULS const transformPass2 = hashSync (passwrod); $2a$10$mynd130vI1vkz4OQ3C.6FeYXGEq24KLUt1CsKN2WZqVsv0tPrtOcWconst transformPass3 = hashSync (passwrod); $2a$10$bOHdFQ4TKBrtcNgmduzD8esds04BoXc0JcrLme68rTeik7U96KBvu
验证密码:使用不同的值 匹配 密码123456,都能通过
const password = '123456' ;const databasePassword1 = '$2a$10$HgTA1GX8uxbocSQlbQ42/.Y2XnIL7FyfKzn6IC69IXveD6F9LiULS' const databasePassword2 = '$2a$10$mynd130vI1vkz4OQ3C.6FeYXGEq24KLUt1CsKN2WZqVsv0tPrtOcW' const databasePassword3 = '$2a$10$bOHdFQ4TKBrtcNgmduzD8esds04BoXc0JcrLme68rTeik7U96KBvu' if (compareSync (password, databasePassword3)) { console .log ('密码通过' ); }
推荐使用bcryptjs,算法要比md5高级
角色权限 RBAC
权限:具备操作某个事务的能力
角色:一系列权限的集合
如:一般的管理系统中: 销售人员:仅仅可以查看商品信息 运营人员:可以查看,修改商品信息 管理人员:可以查看,修改,删除,以及修改员工权限等等 管理人员只要为每个员工账号分配对应的角色,登陆操作时就只能执行对应的权限或看到对应的页面
权限类型
展示(菜单),如:显示用户列表,显示删除按钮等等…
操作(功能),如:增删改查,上传下载,发布公告,发起活动等等…
数据库设计
数据库设计:可简单,可复杂,几个人使用的系统和几千人使用的系统是不一样的 小型项目:用户表,权限表 中型项目:用户表,角色表,权限表 大型项目:用户表,用户分组表,角色表,权限表,菜单表…
没有角色的设计
只有用户表,菜单表,两者是多对多关系,有一个关联表
缺点:
新建一个用户时,在用户表中添加一条数据
新建一个用户时,在关联表中添加N条数据
每次新建一个用户需要添加1+N(关联几个)条数据
如果有100个用户,每个用户100个权限,那需要添加10000条数据
基于RBAC的设计 用户表和角色表的关系设计:
如果你希望一个用户可以有多个角色,如:一个人即是销售总监,也是人事管理,就设计多对多关系 如果你希望一个用户只能有一个角色,就设计一对多,多对一关系
角色表和权限表的关系设计:
一个角色可以拥有多个权限,一个权限被多个角色使用,设计多对多关系
多对多关系设计
用户表与角色表是多对多关系,角色表与菜单表是多对多关系
更加复杂的设计
实现流程
数据表设计
实现角色的增删改查
实现用户的增删改查,增加和修改用户的时候需要选择角色
实现权限的增删改查
实现角色与授权的关联
判断当前登录的用户是否有访问菜单的权限
根据当前登录账户的角色信息动态显示左侧菜单(前端)
代码实现
这里将实现一个用户,部门,角色,权限的例子: 用户通过成为部门的一员,则拥有部门普通角色的权限,还可以单独给用户设置角色,通过角色,获取权限。 权限模块包括,模块,菜单,操作,通过type区分类型,这里就不再拆分。
关系总览:
用户 - 部门:一对多关系,这里设计用户只能加入一个部门,如果设计可以加入多个部门,设计为多对多关系
用户 - 角色:多对多关系,可以给用户设置多个角色
角色 - 部门:多对多关系,一个部门多个角色
角色 - 权限:多对多关系,一个角色拥有多个权限,一个权限被多个角色使用
数据库实体设计 用户
import { Column , Entity , ManyToMany , ManyToOne , JoinColumn , JoinTable , PrimaryGeneratedColumn , } from 'typeorm' ; import { RoleEntity } from '../../role/entities/role.entity' ;import { DepartmentEntity } from '../../department/entities/department.entity' ;@Entity ({ name : 'user' }) export class UsersEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' , length : 30 , nullable : false , unique : true , }) username : string; @Column ({ type : 'varchar' , name : 'password' , length : 100 , nullable : false , select : false , comment : '密码' , }) password : string; @ManyToMany (() => RoleEntity , (role ) => role.users ) @JoinTable ({ name : 'user_role' }) roles : RoleEntity []; @ManyToOne (() => DepartmentEntity , (department ) => department.users ) @JoinColumn ({ name : 'department_id' }) department : DepartmentEntity ; }
角色
import { Entity , PrimaryGeneratedColumn , Column , ManyToMany , JoinTable , } from 'typeorm' ; import { UsersEntity } from '../../user/entities/user.entity' ;import { DepartmentEntity } from '../../department/entities/department.entity' ;import { AccessEntity } from '../../access/entities/access.entity' ;@Entity ({ name : 'role' }) export class RoleEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' , length : 30 }) rolename : string; @ManyToMany (() => UsersEntity , (user ) => user.roles ) users : UsersEntity []; @ManyToMany (() => DepartmentEntity , (department ) => department.roles ) department : DepartmentEntity []; @ManyToMany (() => AccessEntity , (access ) => access.roles ) @JoinTable ({ name : 'role_access' }) access : AccessEntity []; }
部门
import { Entity , PrimaryGeneratedColumn , Column , ManyToMany , OneToMany , JoinTable , } from 'typeorm' ; import { UsersEntity } from '../../user/entities/user.entity' ;import { RoleEntity } from '../../role/entities/role.entity' ;@Entity ({ name : 'department' }) export class DepartmentEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' , length : 30 }) departmentname : string; @OneToMany (() => UsersEntity , (user ) => user.department ) users : UsersEntity []; @ManyToMany (() => RoleEntity , (role ) => role.department ) @JoinTable ({ name : 'department_role' }) roles : RoleEntity []; }
权限
import { Entity , PrimaryGeneratedColumn , Column , Tree , TreeChildren , TreeParent , ManyToMany , } from 'typeorm' ; import { RoleEntity } from '../../role/entities/role.entity' ;@Entity ({ name : 'access' }) @Tree ('closure-table' ) export class AccessEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' , length : 30 , comment : '模块' }) module_name : string; @Column ({ type : 'varchar' , length : 30 , nullable : true , comment : '操作' }) action_name : string; @Column ({ type : 'tinyint' , comment : '类型:1:模块,2:菜单,3:操作' }) type : number; @Column ({ type : 'text' , nullable : true , comment : '操作地址' }) url : string; @TreeParent () parentCategory : AccessEntity ; @TreeChildren () childCategorys : AccessEntity []; @ManyToMany (() => RoleEntity , (role ) => role.access ) roles : RoleEntity []; }
接口实现 由于要实现很多接口,这里只说明一部分,其实都是数据库的操作,所有接口如下:
根据用户的id获取信息 :id,用户名,部门名,角色,这些信息在做用户登陆时传递到token中。
这里设计的是:创建用户时,添加部门,就会成为部门的普通角色,也可单独设置角色,但不是每个用户都有单独的角色。
async getUserinfoByUid (uid: number ) { 获取用户 const user = await this .usersRepository .findOne ( { id : uid }, { relations : ['roles' ] }, ); if (!user) ToolsService .fail ('用户ID不存在' ); const sql = ` select user.id as user_id, user.username, user.department_id, department.departmentname, role.id as role_id, rolename from user, department, role, department_role as dr where user.department_id = department.id and department.id = dr.departmentId and role.id = dr.roleId and user.id = ${uid} ` ; const result = await this .usersRepository .query (sql); const userinfo = result[0 ]; const userObj = { user_id : userinfo.user_id , username : userinfo.username , department_id : userinfo.department_id , departmentname : userinfo.departmentname , roles : [{ id : userinfo.role_id , rolename : userinfo.rolename }], }; if (user.roles .length > 0 ) { const _user = JSON .parse (JSON .stringify (user)); userObj.roles = [...userObj.roles , ..._user.roles ]; } return userObj; } { "status" : 200 , "message" : "请求成功" , "data" : { "user_id" : 1 , "username" : "admin" , "department_id" : 1 , "departmentname" : "销售部" , "roles" : [ { "id" : 1 , "rolename" : "销售部员工" }, { "id" : 5 , "rolename" : "admin" } ] } }
结合possport + jwt 做用户登陆授权验证
在验证账户密码通过后,possport 返回用户,然后根据用户id获取用户信息,存储token,用于路由守卫,还可以使用redis存储,以作他用。
async login (user : any): Promise <any> { const { id } = user; const userResult = await this .userService .getUserinfoByUid (id); const access_token = this .jwtService .sign (userResult); await this .redisService .set (`user-token-${id} ` , access_token, 60 * 60 * 24 ); return { access_token }; } { "status" : 200 , "message" : "请求成功" , "data" : { "access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZGVwYXJ0bWVudF9pZCI6MSwiZGVwYXJ0bWVudG5hbWUiOiLplIDllK7pg6giLCJyb2xlcyI6W3siaWQiOjEsInJvbGVuYW1lIjoi6ZSA5ZSu6YOo5ZGY5belIn0seyJpZCI6NSwicm9sZW5hbWUiOiJhZG1pbiJ9XSwiaWF0IjoxNjIxNjA1Nzg5LCJleHAiOjE2MjE2OTIxODl9.VIp0MdzSPM13eq1Bn8bB9Iu_SLKy4yoMU2N4uwgWDls" } }
后端的权限访问
使用守卫,装饰器,结合token,验证访问权限
逻辑:
第一步:在controller使用自定义守卫装饰接口路径,在请求该接口路径时,全部进入守卫逻辑
第二步:使用自定义装饰器装饰特定接口,传递角色,自定义守卫会使用反射器获取该值,以判断该用户是否有权限
如下:findOne接口使用了自定义装饰器装饰接口,意思是只能admin来访问
import { Controller , Get , Body , Patch , Post , Param , Delete , UseGuards , ParseIntPipe , } from '@nestjs/common' ; import { UserService } from './user.service' ;import { CreateUserDto } from './dto/create-user.dto' ;import { UpdateUserDto } from './dto/update-user.dto' ;import { AuthGuard } from '../../common/guard/auth.guard' ;import { Roles } from '../../common/decorator/role.decorator' ;@UseGuards (AuthGuard ) @Controller ('user' ) export class UserController { constructor (private readonly userService: UserService ) { } @Get () async findAll ( ) { const [data, count] = await this .userService .findAll (); return { count, data }; } @Get (':id' ) @Roles ('admin' ) async findOne (@Param('id' , new ParseIntPipe()) id: number ) { return await this .userService .findOne (id); } }
装饰器
import { SetMetadata } from '@nestjs/common' ;export const Roles = (...args: string[] ) => SetMetadata ('roles' , args);
自定义守卫
返回true则有访问权限,返回false则直接报403
import { CanActivate , ExecutionContext , Injectable } from '@nestjs/common' ;import { JwtService } from '@nestjs/jwt' ;import { Reflector } from '@nestjs/core' ; import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class AuthGuard implements CanActivate { constructor ( private readonly reflector: Reflector, private readonly jwtService: JwtService, ) { } private whiteUrlList : string[] = []; private isWhiteUrl (urlList : string[], url : string): boolean { if (urlList.includes (url)) { return true ; } return false ; } canActivate (context : ExecutionContext ): boolean { const request = context.switchToHttp ().getRequest (); if (this .isWhiteUrl (this .whiteUrlList , request.url )) return true ; const token = context.switchToRpc ().getData ().headers .token ; const user : any = this .jwtService .decode (token); if (!user) ToolsService .fail ('token获取失败,请传递token或书写正确' ); const authRoles = this .reflector .get <string[]>( 'roles' , context.getHandler (), ); if (!authRoles) return true ; const userRoles = user.roles ; for (let i = 0 ; i < userRoles.length ; i++) { if (authRoles.includes (userRoles[i].rolename )) { return true ; } } return false ; } }
简单测试
两个用户,分别对应不同的角色,分别请求user的findOne接口 用户1:销售部员工和admin 用户2:人事部员工
用户1:销售部员工和admin { "status": 200, "message": "请求成功", "data": { "user_id": 1, "username": "admin", "department_id": 1, "departmentname": "销售部", "roles": [ { "id": 1, "rolename": "销售部员工" }, { "id": 5, "rolename": "admin" } ] } } 用户2:人事部员工 { "status": 200, "message": "请求成功", "data": { "user_id": 2, "username": "admin2", "department_id": 2, "departmentname": "人事部", "roles": [ { "id": 3, "rolename": "人事部员工" } ] } } 不出意外的话:2号用户的请求结果 { "status": 403, "message": "Forbidden resource", "error": "Forbidden", "path": "/user/1", "timestamp": "2021-05-21T14:44:04.954Z" }
前端的权限访问则是通过权限表url和type来处理
定时任务 nest如何开启定时任务?
定时任务场景
每天定时更新,定时发送邮件
没有controller,因为定时任务是自动完成的
yarn add @nestjs/schedule
import { Module } from '@nestjs/common' ;import { TasksService } from './tasks.service' ;@Module ({ providers : [TasksService ], }) export class TasksModule {}
在这里编写你的定时任务
import { Injectable , Logger } from '@nestjs/common' ;import { Cron , Interval , Timeout } from '@nestjs/schedule' ;@Injectable () export class TasksService { private readonly logger = new Logger (TasksService .name ); @Cron ('45 * * * * *' ) 每隔45 秒执行一次 handleCron ( ) { this .logger .debug ('Called when the second is 45' ); } @Interval (10000 ) 每隔10 秒执行一次 handleInterval ( ) { this .logger .debug ('Called every 10 seconds' ); } @Timeout (5000 ) 5 秒只执行一次 handleTimeout ( ) { this .logger .debug ('Called once after 5 seconds' ); } }
自定义定时时间
* * * * * * 分别对应的意思: 第1 个星:秒 第2 个星:分钟 第3 个星:小时 第4 个星:一个月中的第几天 第5 个星:月 第6 个星:一个星期中的第几天 如: 45 * * * * *:每隔45 秒执行一次
挂载-使用
import { TasksModule } from './tasks/task.module' ;import { ScheduleModule } from '@nestjs/schedule' ;imports : [ ConfigModule .load (path.resolve (__dirname, 'config' , '**/!(*.d).{ts,js}' )), ScheduleModule .forRoot (), TasksModule , ],
接入Swagger接口文档
优点:不用写接口文档,在线生成,自动生成,可操作数据库,完美配合dto
缺点:多一些代码,显得有点乱,习惯就好
yarn add @nestjs/swagger swagger-ui-express -D
import { SwaggerModule , DocumentBuilder } from '@nestjs/swagger' ;async function bootstrap ( ) { const app = await NestFactory .create <NestExpressApplication >(AppModule ); const options = new DocumentBuilder () .addBearerAuth () .setTitle ('接口文档' ) .setDescription ('接口文档介绍' ) .addServer ('http://localhost:9000' , '开发环境' ) .addServer ('https://test.com/release' , '正式环境' ) .setVersion ('1.0.0' ) .setContact ('poetry' , '' , 'test@qq.com' ) .build (); const document = SwaggerModule .createDocument (app, options, { extraModels : [], }); SwaggerModule .setup ('api-docs' , app, document ); const PORT = process.env .PORT || 9000 ; await app.listen (PORT , () => Logger .log (`服务已经启动 http://localhost:${PORT} ` ), ); } bootstrap ();
swagger装饰器
https://swagger.io/
@ApiTags ('user' ) @ApiOperation ({ summary : '标题' , description : '详细描述' }) @ApiQuery ({ name : 'limit' , required : true }) @ApiQuery ({ name : 'role' , enum : UserRole }) @ApiParam ({ name : 'id' }) @ApiBody ({ type : UserCreateDTO , description : '输入用户名和密码' }) @ApiResponse ({ status : 200 , description : '成功返回200,失败返回400' , type : UserCreateDTO , }) @ApiProperty ({ example : 'Kitty' , description : 'The name of the Cat' }) name : string;
在controller引入@nestjs/swagger, 并配置@ApiBody() 和 @ApiParam() 不写也是可以的
user.controller .ts import { Controller , Get , Post , Body , Patch , Query , Param , Delete , HttpCode , HttpStatus , ParseIntPipe , } from '@nestjs/common' ; import { ApiOperation , ApiTags , ApiQuery , ApiBody , ApiResponse , } from '@nestjs/swagger' ; import { UserService } from './user.service' ;import { CreateUserDto } from './dto/create-user.dto' ;import { UpdateUserDto } from './dto/update-user.dto' ;@Controller ('user' ) @ApiTags ('user' ) export class UserController { constructor (private readonly userService: UserService ) { } @Post () @ApiOperation ({ summary : '创建用户' , description : '创建用户' }) @HttpCode (HttpStatus .OK ) async create (@Body() user: CreateUserDto ) { return await this .userService .create (user); } @Get () @ApiOperation ({ summary : '查找全部用户' , description : '创建用户' }) @ApiQuery ({ name : 'limit' , required : true }) 请求参数 @ApiQuery ({ name : 'offset' , required : true }) 请求参数 async findAll (@Query() query ) { console .log (query); const [data, count] = await this .userService .findAll (query); return { count, data }; } @Get (':id' ) @ApiOperation ({ summary : '根据ID查找用户' }) async findOne (@Param('id' , new ParseIntPipe()) id: number ) { return await this .userService .findOne (id); } @Patch (':id' ) @ApiOperation ({ summary : '更新用户' }) @ApiBody ({ type : UpdateUserDto , description : '参数可选' }) 请求体 @ApiResponse ({ 响应示例 status : 200 , description : '成功返回200,失败返回400' , type : UpdateUserDto , }) async update ( @Param('id' , new ParseIntPipe()) id: number, @Body() user: UpdateUserDto, ) { return await this .userService .update (id, user); } @Delete (':id' ) @ApiOperation ({ summary : '删除用户' }) async remove (@Param('id' , new ParseIntPipe()) id: number ) { return await this .userService .remove (id); } }
编写dto,引入@nestjs/swagger
创建
import { IsNotEmpty , MinLength , MaxLength } from 'class-validator' ;import { ApiProperty } from '@nestjs/swagger' ;export class CreateUserDto { @ApiProperty ({ example : 'kitty' , description : '用户名' }) 添加这里即可 @IsNotEmpty ({ message : '用户名不能为空' }) username : string; @ApiProperty ({ example : '12345678' , description : '密码' }) @IsNotEmpty ({ message : '密码不能为空' }) @MinLength (6 , { message : '密码长度不能小于6位' , }) @MaxLength (20 , { message : '密码长度不能超过20位' , }) password : string; }
更新
import { IsEnum , MinLength , MaxLength , IsOptional , ValidateIf , IsEmail , IsMobilePhone , } from 'class-validator' ; import { ApiProperty } from '@nestjs/swagger' ;import { Type } from 'class-transformer' ;export class UpdateUserDto { @ApiProperty ({ description : '用户名' , example : 'kitty' , required : false }) 不是必选的 @IsOptional () username : string; @ApiProperty ({ description : '密码' , example : '12345678' , required : false }) @IsOptional () @MinLength (6 , { message : '密码长度不能小于6位' , }) @MaxLength (20 , { message : '密码长度不能超过20位' , }) password : string; @ApiProperty ({ description : '邮箱' , example : 'llovenest@163.com' , required : false , }) @IsOptional () @IsEmail ({}, { message : '邮箱格式错误' }) @ValidateIf ((o ) => o.username === 'admin' ) email : string; @ApiProperty ({ description : '手机号码' , example : '13866668888' , required : false , }) @IsOptional () @IsMobilePhone ('zh-CN' , {}, { message : '手机号码格式错误' }) mobile : string; @ApiProperty ({ description : '性别' , example : 'female' , required : false , enum : ['male' , 'female' ], }) @IsOptional () @IsEnum (['male' , 'female' ], { message : 'gender只能传入字符串male或female' , }) gender : string; @ApiProperty ({ description : '状态' , example : 1 , required : false , enum : [0 , 1 ], }) @IsOptional () @IsEnum ( { 禁用: 0 , 可用: 1 }, { message : 'status只能传入数字0或1' , }, ) @Type (() => Number ) status : number; }
打开:localhost:3000/api-docs,开始测试接口
数据库 nest连接Mongodb mac中,直接使用brew install mongodb-community安装MongoDB,然后启动服务brew services start mongodb-community 查看服务已经启动ps aux | grep mongo
Nestjs中操作Mongodb数据库可以使用Nodejs封装的DB库,也可以使用Mongoose。
npm install --save @nestjs/mongoose mongoose npm install --save-dev @types/mongoose
在app.module.ts中配置数据库连接
import { ConfigModule , ConfigService } from 'nestjs-config' ;import { MongooseModule } from '@nestjs/mongoose' ;import { MongodbModule } from '../examples/mongodb/mongodb.module' ;@Module ({ imports : [ ConfigModule .load (resolve (__dirname, 'config' , '**/!(*.d).{ts,js}' )), MongooseModule .forRootAsync ({ useFactory : async (configService : ConfigService ) => configService.get ('mongodb' ), inject : [ConfigService ], }), MongodbModule , ], controllers : [], providers : [], }) export class AppModule implements NestModule {}
export default { uri : 'mongodb://localhost:27017/nest' , };
配置Schema
import * as mongoose from 'mongoose' ;export const ArticleSchema = new mongoose.Schema ({ title : String , content :String , author : String , status : Number , });
在控制器对应的Module中配置Model
import { Module } from '@nestjs/common' ;import { MongodbService } from './mongodb.service' ;import { MongodbController } from './mongodb.controller' ;import { ArticleSchema } from './schemas/article.schema' ;import { MongooseModule } from '@nestjs/mongoose' ;@Module ({ imports : [ MongooseModule .forFeature ([ { name : 'Article' , schema : ArticleSchema , collection : 'article' , }, ]), ], controllers : [MongodbController ], providers : [MongodbService ], }) export class MongodbModule {}
在服务里面使用@InjectModel 获取数据库Model实现操作数据库
import { Injectable } from '@nestjs/common' ;import { InjectModel } from '@nestjs/mongoose' ;@Injectable () export class MongodbService { constructor (@InjectModel('Article' ) private readonly articleModel ) {} async findAll ( ) { return await this .articleModel .find ().exec (); } async findById (id ) { return await this .articleModel .findById (id); } async create (body ) { return await this .articleModel .create (body); } async update (body ) { const { id, ...params } = body; return await this .articleModel .findByIdAndUpdate (id, params); } async delete (id ) { return await this .articleModel .findByIdAndDelete (id); } }
浏览器测试 http://localhost:9000/api/mongodb/list
typeORM操作Mysql数据库 mac中,直接使用brew install mysql安装mysql,然后启动服务brew services start mysql 查看服务已经启动ps aux | grep mysql
Nest 操作Mysql官方文档:https://docs.nestjs.com/techniques/database
npm install --save @nestjs/typeorm typeorm mysql
配置数据库连接地址
const { MYSQL_HOST , MYSQL_PORT , MYSQL_USER , MYSQL_PASSWORD , MYSQL_DATABASE } = process.env ; const config = { type : 'mysql' , host : MYSQL_HOST , port : MYSQL_PORT , username : MYSQL_USER , password : MYSQL_PASSWORD , database : MYSQL_DATABASE , synchronize : process.env .NODE_ENV !== 'production' , autoLoadEntities : true , keepConnectionAlive : true , retryDelay : 3000 , retryAttempts : 10 , dateStrings : 'DATETIME' , timezone : '+0800' , entities : ['dist/**/*.entity{.ts,.js}' ], }; export default config;
import { resolve, join } from 'path' ;import { ConfigModule , ConfigService } from 'nestjs-config' ;import { TypeOrmModule } from '@nestjs/typeorm' ;@Module ({ imports : [ ConfigModule .load (resolve (__dirname, 'config' , '**/!(*.d).{ts,js}' )), TypeOrmModule .forRootAsync ({ useFactory : (config: ConfigService ) => config.get ('typeorm' ), inject : [ConfigService ], }), ], controllers : [], providers : [], }) export class AppModule implements NestModule {}
配置实体entity
import { Column , Entity , ManyToMany , OneToMany , PrimaryGeneratedColumn , } from 'typeorm' ; import { PostsEntity } from './post.entity' ;@Entity ('photo' ) export class PhotoEntity { @PrimaryGeneratedColumn ('uuid' ) id : string; @Column ({ length : 50 }) url : string; @ManyToMany (() => PostsEntity , (post ) => post.photos ) posts : PostsEntity ; } import { Column , Entity , OneToMany , PrimaryGeneratedColumn } from 'typeorm' ;import { PhotoEntity } from './photo.entity' ;export type UserRoleType = 'admin' | 'editor' | 'ghost' ;export type postStatus = 1 | 2 | 3 ;@Entity ('posts' ) export class PostsEntity { @PrimaryGeneratedColumn ('uuid' ) id : string; @Column ({ length : 50 }) title : string; @Column ({ length : 18 }) author : string; @Column ({ type : 'longtext' , default : null }) content : string; @Column ({ default : null }) cover_url : string; @Column ({ default : 0 }) type : number; @Column ({ type : 'text' , default : null }) remark : string; @Column ({ type : 'enum' , enum : [1 , 2 , 3 ], default : 1 , }) status : postStatus; @Column ({ type : 'timestamp' , default : () => 'CURRENT_TIMESTAMP' }) create_time : Date ; @Column ({ type : 'timestamp' , default : () => 'CURRENT_TIMESTAMP' , }) update_time : Date ; @Column ({ type : 'enum' , enum : ['admin' , 'editor' , 'ghost' ], default : 'ghost' , select : false , }) role : UserRoleType ; @OneToMany (() => PhotoEntity , (photo ) => photo.posts ) photos : []; }
参数校验
Nest 与 class-validator 配合得很好。这个优秀的库允许您使用基于装饰器的验证。装饰器的功能非常强大,尤其是与 Nest 的 Pipe 功能相结合使用时,因为我们可以通过访问 metatype 信息做很多事情,在开始之前需要安装一些依赖。
npm i --save class-validator class-transformer
import { ApiProperty , ApiPropertyOptional } from '@nestjs/swagger' ;import { IsNotEmpty , IsNumber , IsString } from 'class-validator' ;export class CreatePostDto { @IsNotEmpty ({ message : '文章标题必填' }) readonly title : string; @IsNotEmpty ({ message : '缺少作者信息' }) readonly author : string; readonly content : string; readonly cover_url : string; @IsNotEmpty ({ message : '缺少文章类型' }) readonly type : number; readonly remark : string; }
在控制器对应的Module中配置Model
import { Module } from '@nestjs/common' ;import { TypeOrmModule } from '@nestjs/typeorm' ;import { PostsService } from './posts.service' ;import { PostsController } from './posts.controller' ;import { PostsEntity } from './entities/post.entity' ;@Module ({ imports : [TypeOrmModule .forFeature ([PostsEntity ])], controllers : [PostsController ], providers : [PostsService ], }) export class PostsModule {}
在服务里面使用@InjectRepository获取数据库Model实现操作数据库
import { HttpException , HttpStatus , Injectable } from '@nestjs/common' ;import { InjectRepository } from '@nestjs/typeorm' ;import { Repository , Not , Between , Equal , Like , In } from 'typeorm' ;import * as dayjs from 'dayjs' ;import { CreatePostDto } from './dto/create-post.dto' ;import { UpdatePostDto } from './dto/update-post.dto' ;import { PostsEntity } from './entities/post.entity' ;import { PostsRo } from './interfaces/posts.interface' ;@Injectable () export class PostsService { constructor ( @InjectRepository(PostsEntity) private readonly postsRepository: Repository<PostsEntity>, ) {} async create (post: CreatePostDto ) { const { title } = post; const doc = await this .postsRepository .findOne ({ where : { title } }); console .log ('doc' , doc); if (doc) { throw new HttpException ('文章标题已存在' , HttpStatus .BAD_REQUEST ); } return { data : await this .postsRepository .save (post), message : '创建成功' , }; } async findAll (query = {} as any ) { let { pageSize, pageNum, orderBy, sort, ...params } = query; orderBy = query.orderBy || 'create_time' ; sort = query.sort || 'DESC' ; pageSize = Number (query.pageSize || 10 ); pageNum = Number (query.pageNum || 1 ); console .log ('query' , query); const queryParams = {} as any; Object .keys (params).forEach ((key ) => { if (params[key]) { queryParams[key] = Like (`%${params[key]} %` ); } }); const qb = await this .postsRepository .createQueryBuilder ('post' ); qb.where (queryParams); qb.orderBy (`post.${orderBy} ` , sort); qb.skip (pageSize * (pageNum - 1 )); qb.take (pageSize); return { list : await qb.getMany (), totalNum : await qb.getCount (), total : await this .postsRepository .count (), pageSize, pageNum, }; } async findById (id : string): Promise <PostsEntity > { return await this .postsRepository .findOne ({ where : { id } }); } async update (id: string, updatePostDto: UpdatePostDto ) { const existRecord = await this .postsRepository .findOne ({ where : { id } }); if (!existRecord) { throw new HttpException (`id为${id} 的文章不存在` , HttpStatus .BAD_REQUEST ); } const updatePost = this .postsRepository .merge (existRecord, { ...updatePostDto, update_time : dayjs ().format ('YYYY-MM-DD HH:mm:ss' ), }); return { data : await this .postsRepository .save (updatePost), message : '更新成功' , }; } async remove (id: string ) { const existPost = await this .postsRepository .findOne ({ where : { id } }); if (!existPost) { throw new HttpException (`文章ID ${id} 不存在` , HttpStatus .BAD_REQUEST ); } await this .postsRepository .remove (existPost); return { data : { id }, message : '删除成功' , }; } }
nest统一处理数据库操作的查询结果
操作数据库时,如何做异常处异常? 比如id不存在,用户名已经存在?如何统一处理请求失败和请求成功?
处理方式 :
在nest中,一般是在service 中处理异常,如果有异常,直接抛出错误,由过滤器 捕获,统一格式返回,如果成功,service把结果返回,controller直接return结果即可,由拦截器 捕获,统一格式返回
失败:过滤器统一处理
成功:拦截器统一处理
当然你也可以在controller处理
import { Controller , Get , Post , Body , HttpCode , HttpStatus , } from '@nestjs/common' ; import { UserService } from './user.service' ;@Controller ('user' ) export class UserController { constructor (private readonly userService: UserService ) { } @Post () @HttpCode (HttpStatus .OK ) async create (@Body() user ) { return await this .userService .create (user); } @Get (':id' ) async findOne (@Param('id' ) id: string ) { return await this .userService .findOne (id); } }
import { Injectable , HttpException , HttpStatus } from '@nestjs/common' ;import { Repository } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity> ) { } async create (user ) { const { username } = user; const result = await this .usersRepository .findOne ({ username }); if (result) { throw new HttpException ( { message : '请求失败' , error : '用户名已存在' }, HttpStatus .BAD_REQUEST , ); } return await this .usersRepository .save (user); } async findOne (id: string ) { const result = await this .usersRepository .findOne (id); if (!result) { throw new HttpException ( { message : '请求失败' , error : '用户id不存在' }, HttpStatus .BAD_REQUEST , ); } return result; } }
可以将HttpException再简单封装一下,或者使用继承,这样代码更简洁一些
import { Injectable , HttpException , HttpStatus } from '@nestjs/common' ;@Injectable () export class ToolsService { static fail (error, status = HttpStatus.BAD_REQUEST ) { throw new HttpException ( { message : '请求失败' , error : error, }, status, ); } }
简洁代码
import { Injectable , HttpException , HttpStatus } from '@nestjs/common' ;import { Repository } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity> ) { } async create (user ) { const { username } = user; const result = await this .usersRepository .findOne ({ username }); if (result) ToolsService .fail ('用户名已存在' ); return await this .usersRepository .save (user); } async findOne (id: string ) { const result = await this .usersRepository .findOne (id); if (!result) ToolsService .fail ('用户id不存在' ); return result; } }
全局使用filter过滤器
import { ArgumentsHost , Catch , ExceptionFilter , HttpException , } from '@nestjs/common' ; @Catch () export class HttpExceptionFilter implements ExceptionFilter { catch (exception : HttpException , host : ArgumentsHost ) { const ctx = host.switchToHttp (); const response = ctx.getResponse (); const request = ctx.getRequest (); const status = exception.getStatus (); const exceptionRes : any = exception.getResponse (); const { error, message } = exceptionRes; const msgLog = { status, message, error, path : request.url , timestamp : new Date ().toISOString (), }; response.status (status).json (msgLog); } }
全局使用interceptor拦截器
import { CallHandler , ExecutionContext , Injectable , NestInterceptor , } from '@nestjs/common' ; import { map } from 'rxjs/operators' ;import { Observable } from 'rxjs' ;@Injectable () export class AuthInterceptor implements NestInterceptor { intercept (context : ExecutionContext , next : CallHandler ): Observable <any> { return next.handle ().pipe ( map ((data ) => { return { status : 200 , message : '请求成功' , data : data, }; }), ); } }
import { HttpExceptionFilter } from './common/filters/http-exception.filter' ;import { TransformInterceptor } from './common/interceptors/transform.interceptor' ;async function bootstrap ( ) { const app = await NestFactory .create <NestExpressApplication >(AppModule ); app.useGlobalFilters (new HttpExceptionFilter ()); app.useGlobalInterceptors (new TransformInterceptor ()); const PORT = process.env .PORT || 9000 ; await app.listen (PORT , () => Logger .log (`服务已经启动 http://localhost:${PORT} ` ), ); } bootstrap ();
失败
成功
数据库实体设计与操作
typeorm的数据库实体如何编写? 数据库实体的监听装饰器如何使用?
实体设计 简单例子:下面讲解
import { Entity , PrimaryGeneratedColumn , Column , CreateDateColumn , UpdateDateColumn } from "typeorm" ;@Entity ({ name : 'users' }) export class User { @PrimaryGeneratedColumn () id : number; @Column () username : string; @Column () password : string; @Column () status : boolean; @CreateDateColumn () created_at :date; @UpdateDateColumn () updated_at :date; @DeleteDateColumn () deleted_at :date; }
装饰器说明
Entity 实体声明,程序运行时,自动创建的数据库表,@Entity({ name: 'users' }), name则是给该表命名,否则自动命名
PrimaryColumn 设置主键,没有自增
PrimaryGeneratedColumn 设置主键和自增,一般是id
Column 设置数据库列字段,在下面说明
CreateDateColumn 创建时间,自动填写
UpdateDateColumn 更新时间,自动填写
DeleteDateColumn 删除时间,自动填写
列字段参数
@Column ("int" ) @Column ("varchar" , { length : 200 }) @Column ({ type : "int" , length : 200 }) @Column ({ type : 'varchar' , name : 'password' , length : 30 , nullable : false , select:false , comment : '密码' }) password :string;@Column ({ type :'varchar' , unique : true , }) username :string;@Column ({ type :'tinyint' , default : () => 1 , comment : '0:禁用,1:可用' }) status :number;@Column ({ type : 'enum' , enum : ['male' , 'female' ], default : 'male' 默认值 }) gender :string;
完整例子
import { Column , Entity , PrimaryGeneratedColumn , CreateDateColumn , UpdateDateColumn , } from 'typeorm' ; @Entity ({ name : 'users' }) export class UsersEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' , length : 30 , nullable : false , unique : true , }) username : string; @Column ({ type : 'varchar' , name : 'password' , length : 100 , nullable : false , select : false , comment : '密码' , }) password : string; @Column ({ type : 'varchar' , length : 11 , select : false , nullable : true , comment : '手机号码' , }) mobile : string; @Column ({ type : 'varchar' , length : 50 , select : false , nullable : true , comment : '邮箱' , }) email : string; @Column ({ type : 'enum' , enum : ['male' , 'female' ], default : 'male' , }) gender : string; @Column ({ type : 'tinyint' , default : () => 1 , comment : '0:禁用,1:可用' , }) status : number; @CreateDateColumn ({ type : 'timestamp' , nullable : false , name : 'created_at' , comment : '创建时间' , }) createdAt : Date ; @UpdateDateColumn ({ type : 'timestamp' , nullable : false , name : 'updated_at' , comment : '更新时间' , }) updatedAt : Date ; @DeleteDateColumn ({ type : 'timestamp' , nullable : true , name : 'deleted_at' , comment : '删除时间' , }) deletedAt : Date ; }
抽离部分重复的字段:使用继承
baseEntity:将id,创建时间,更新时间,删除时间抽离成BaseEntity
import { Entity , PrimaryGeneratedColumn , CreateDateColumn , UpdateDateColumn , DeleteDateColumn , } from 'typeorm' ; @Entity () export class BaseEntity { @PrimaryGeneratedColumn () id : number; @CreateDateColumn ({ type : 'timestamp' , nullable : false , name : 'created_at' , comment : '创建时间' , }) createdAt : Date ; @UpdateDateColumn ({ type : 'timestamp' , nullable : false , name : 'updated_at' , comment : '更新时间' , }) updatedAt : Date ; @DeleteDateColumn ({ type : 'timestamp' , nullable : false , name : 'deleted_at' , comment : '删除时间' , }) deletedAt : Date ; }
users表继承自baseEntity,就不需要写创建时间,修改时间,自增ID等重复字段了。其他的表也可以继承自baseEntity,减少重复代码
import { Column ,Entity } from 'typeorm' ;import { BaseEntity } from './user.baseEntity' ;@Entity ({ name : 'users' }) export class UsersEntity extends BaseEntity { @Column ({ type : 'varchar' , length : 30 , nullable : false , unique : true , }) username : string; @Column ({ type : 'varchar' , name : 'password' , length : 100 , nullable : false , select : false , comment : '密码' , }) password : string; @Column ({ type : 'varchar' , length : 11 , select : false , nullable : true , comment : '手机号码' , }) mobile : string; @Column ({ type : 'varchar' , length : 50 , select : false , nullable : true , comment : '邮箱' , }) email : string; @Column ({ type : 'enum' , enum : ['male' , 'female' ], default : 'male' , }) gender : string; @Column ({ type : 'tinyint' , default : () => 1 , comment : '0:禁用,1:可用' , }) status : number; }
实体监听装饰器
其实是typeorm在操作数据库时的生命周期,可以更方便的操作数据
查找后:@AfterLoad
插入前:@BeforeInsert
插入后:@AfterInsert
更新前:@BeforeUpdate
更新后:@AfterUpdate
删除前:@BeforeRemove
AfterLoad例子:其他的装饰器是一样的用法
import { Column , Entity , AfterLoad , } from 'typeorm' ; @Entity ({ name : 'users' }) export class UsersEntity extends BaseEntity { @AfterLoad () load ( ) { console .log ('this' , this ); if (this .age < 20 ) { this .age = 20 ; } } @Column () username : string; @Column () password : string; @Column ({ type : 'tinyint' , default : () => 18 , }) age : number; } { "status" : 200 , "message" : "请求成功" , "data" : { "id" : 1 , "username" : "admin" , "age" : 20 , } }
typeorm增删改查操作
访问数据库的方式有哪些? typeorm增删改查操作的方式有哪些?
多种访问数据库的方式 第一种:Connection
import { Injectable } from '@nestjs/common' ;import { Connection } from 'typeorm' ;import { UsersEntity } from './entities/user.entity' ;@Injectable () export class UserService { constructor ( private readonly connection: Connection, ) { } async test ( ) { return await this .connection .getRepository (UsersEntity ) .findOne ({ where : { id : 1 } }); return await this .connection .createQueryBuilder () .select ('user' ) .from (UsersEntity , 'user' ) .where ('user.id = :id' , { id : 1 }) .getOne (); } }
第二种:Repository,需要@nestjs/typeorm的InjectRepository来注入实体
import { Injectable } from '@nestjs/common' ;import { Repository } from 'typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { InjectRepository } from '@nestjs/typeorm' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) 注入实体 private readonly usersRepository: Repository<UsersEntity>, ) { } async test ( ) { return await this .usersRepository .find ({ where : { id : 1 } }); return await this .usersRepository .createQueryBuilder ('user' ) .where ('id = :id' , { id : 1 }) .getOne (); } }
第三种:getConnection():语法糖,是Connection类型
import { Injectable } from '@nestjs/common' ;import { getConnection } from 'typeorm' ;import { UsersEntity } from './entities/user.entity' ;@Injectable () export class UserService { async test ( ) { return await getConnection () .getRepository (UsersEntity ) .find ({ where : { id : 1 } }); return await getConnection () .createQueryBuilder () .select ('user' ) .from (UsersEntity , 'user' ) .where ('user.id = :id' , { id : 1 }) .getOne (); } }
第四种:getRepository:语法糖
import { Injectable } from '@nestjs/common' ;import { getRepository } from 'typeorm' ;import { UsersEntity } from './entities/user.entity' ;@Injectable () export class UserService { async test ( ) { return await getRepository (UsersEntity ).find ({ where : { id : 1 } }); return await getRepository (UsersEntity ) .createQueryBuilder ('user' ) .where ('user.id = :id' , { id : 1 }) .getOne (); } }
第五种:getManager
import { Injectable } from '@nestjs/common' ;import { getManager } from 'typeorm' ;import { UsersEntity } from './entities/user.entity' ;@Injectable () export class UserService { async test ( ) { return await getManager ().find (UsersEntity , { where : { id : 1 } }); return await getManager () .createQueryBuilder (UsersEntity , 'user' ) .where ('user.id = :id' , { id : 1 }) .getOne (); } }
简单总结
使用的方式太多,建议使用:2,4,比较方便
Connection核心类:
connection 等于getConnection
connection.manager 等于getManager, 等于getConnection.manager
connection.getRepository 等于getRepository, 等于getManager.getRepository
connection.createQueryBuilder 使用QueryBuilder
connection.createQueryRunner 开启事务
EntityManager 和 Repository都封装了操作数据的方法,注意:两者的使用方式是不一样的,(实在不明白搞这么多方法做什么,学得头大)getManager是EntityManager的类型,getRepository是Repository的类型
都可以使用createQueryBuilder,但使用的方式略有不同
增删改查的三种方式
第一种:使用sql语句,适用于sql语句熟练的同学
第二种:typeorm封装好的方法,增删改 + 简单查询
第三种:QueryBuilder查询生成器,适用于关系查询,多表查询,复杂查询
其实底层最终都会生成sql语句,只是封装了几种方式而已,方便人们使用。
第一种:sql语句
export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async findAll ( ) { return await this .usersRepository .query ('select * from users' ); } }
第二种:typeorm封装好的api方法
这里使用第二种访问数据库的方式
export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async findAll ( ) { return await this .usersRepository .findAndCount (); } }
api方法
增 save (user) 创建:返回该数据的所有字段insert (user) 快速插入一条数据,插入成功:返回插入实体,与save方法不同的是,它不执行级联、关系和其他操作。删 remove (user) 删除:返回该数据的可见字段softRemove (user); 拉黑:返回该数据的可见字段,该删除实体必须拥有@DeleteDateColumn ()字段,被拉黑的用户还存在数据库中,但无法被find查找到,会在@DeleteDateColumn ()字段中添加删除时间,可使用recover恢复改 update (id, user) 更新:返回更新实体,不是该数据的字段恢复 recover ({ id }) 恢复:返回id,将被softRemove删除(拉黑)的用户恢复,恢复成功后可以被find查找到查找全部 find ()find ({id :9 }) 条件查找,写法一,找不到返回空对象find ({where :{id :10 }}) 条件查找,写法二,找不到返回空对象findAndCount () 返回数据和总的条数查找一个 findOne (id); 根据ID 查找,找不到返回undefined findOne ({ where : { username } }); 条件查找,找不到返回undefined 根据ID 查找一个或多个 findByIds ([1 ,2 ,3 ]); 查找n个,全部查找不到返回空数组,找到就返回找到的其他 hasId (new UsersEntity ()) 检测实体是否有合成ID ,返回布尔值getId (new UsersEntity ()) 获取实体的合成ID ,获取不到返回undefined create ({username : 'admin12345' , password : '123456' ,}) 创建一个实体,需要调用save保存count ({ status : 1 }) 计数,返回数量,无返回0 increment ({ id }, 'age' , 2 ); 增加,给条件为id的数据的age字段增加2 ,成功返回改变实体decrement ({ id }, 'age' , 2 ) 减少,给条件为id的数据的age字段增加2 ,成功返回改变实体谨用 findOneOrFail (id) 找不到直接报500 错误,无法使用过滤器拦截错误,不要使用clear () 清空该数据表,谨用!!!
find更多参数
this .userRepository .find ({ select : ["firstName" , "lastName" ], 要的字段 relations : ["photos" , "videos" ], 关系查询 where : { 条件查询 firstName : "Timber" , lastName : "Saw" }, where : [{ username : "li" }, { username : "joy" }], 多个条件or, 等于:where username = 'li' or username = 'joy' order : { 排序 name : "ASC" , id : "DESC" }, skip : 5 , 偏移量 take : 10 , 每页条数 cache : 60000 启用缓存:1 分钟 });
find进阶选项
TypeORM 提供了许多内置运算符,可用于创建更复杂的查询
import { Not , Between , In } from "typeorm" ;return await this .usersRepository .find ({ username : Not ('admin' ), }); 将执行以下查询: SELECT * FROM "users" WHERE "username" != 'admin' return await this .usersRepository .find ({ likes : Between (1 , 10 ) }); SELECT * FROM "users" WHERE "likes" BETWEEN 1 AND 10 return await this .usersRepository .find ({ username : In (['admin' , 'admin2' ]), }); SELECT * FROM "users" WHERE "title" IN ('admin' , 'admin2' )
更多查看官网
第三种 :QueryBuilder查询生成器
使用链式操作
QueryBuilder增,删,改
return await this .usersRepository .createQueryBuilder () .insert () 声明插入操作 .into (UsersEntity ) 插入的实体 .values ([ 插入的值,可插入多个 { username : 'Timber' , password : '123456' }, { username : 'Timber2' , password : '123456' }, ]) .execute (); 执行 return this .usersRepository .createQueryBuilder () .update (UsersEntity ) .set ({ username : 'admin22' }) .where ('id = :id' , { id : 2 }) .execute (); return this .usersRepository .createQueryBuilder () .delete () .from (UsersEntity ) .where ('id = :id' , { id : 8 }) .execute (); "raw" : { "fieldCount" : 0 , "affectedRows" : 2 , "insertId" : 13 , "serverStatus" : 2 , "warningCount" : 0 , "message" : "&Records: 2 Duplicates: 0 Warnings: 0" , "protocol41" : true , "changedRows" : 0 }
查询
简单例子
export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, ) { } async findAll ( ) { return await this .usersRepository .createQueryBuilder ('user' ) 创建生成器,参数:别名 .where ('user.id = :id' , { id : id }) 条件 .innerJoinAndSelect ('user.avatar' , 'avatar' ) 关系查询 .addSelect ('user.password' ) 添加显示字段 .getOne (); 获取一条数据 } }
QueryBuilder查询生成器说明
查询单表
访问数据库的方式不同: 方式一:没有指定实体,需要使用from 指定实体 return await getConnection () .createQueryBuilder () .select ('user.username' ) ‘user’:全部字段,‘user.username ’:只获取username .from (UsersEntity , 'user' ) 参1 :连接的实体, 参2 :别名 .where ('user.id = :id' , { id : 1 }) .getOne (); 方式二:指定实体:默认获取全部字段 return await getConnection () .createQueryBuilder (UsersEntity , 'user' ) 指定实体 .where ('user.id = :id' , { id : 1 }) .getOne (); 方式三: 已经在访问时指定了实体:默认获取全部字段 return await this .usersRepository .createQueryBuilder ('user' ) 别名 .where ('user.id = :id' , { id : 1 }) .getOne ();
获取结果
.getSql (); 获取实际执行的sql语句,用于开发时检查问题 .getOne (); 获取一条数据(经过typeorm的字段处理) .getMany (); 获取多条数据 .getRawOne (); 获取一条原数据(没有经过typeorm的字段处理) .getRawMany (); 获取多条原数据 .stream (); 返回流数据 如:经过typeorm的字段处理,获取到的就是实体设计时的字段 { "status" : 200 , "message" : "请求成功" , "data" : { "id" : 1 , "username" : "admin" , "gender" : "male" , "age" : 18 , "status" : 1 , "createdAt" : "2021-04-26T09:58:54.469Z" , "updatedAt" : "2021-04-28T14:47:36.000Z" , "deletedAt" : null } } 如:没有经过typeorm的字段处理,将数据库的字段原生不动的显示出来 { "status" : 200 , "message" : "请求成功" , "data" : { "user_id" : 1 , "user_username" : "admin" , "user_gender" : "male" , "user_age" : 18 , "user_status" : 1 , "user_created_at" : "2021-04-26T09:58:54.469Z" , "user_updated_at" : "2021-04-28T14:47:36.000Z" , "user_deleted_at" : null } }
查询部分字段
.select (["user.id" , "user.name" ]) 实际执行的sql语句:SELECT user.id , user.name FROM users user; 添加隐藏字段:实体中设置select为false 时,是不显示字段,使用addSelect会将字段显示出来 .addSelect ('user.password' )
where条件
.where ("user.name = :name" , { name : "joy" }) 等于 .where ("user.name = :name" ) .setParameter ("name" , "Timber" ) 实际执行的sql语句:SELECT * FROM users user WHERE user.name = 'joy' 多个条件 .where ("user.firstName = :firstName" , { firstName : "Timber" }) .andWhere ("user.lastName = :lastName" , { lastName : "Saw" }); 实际执行的sql语句:SELECT * FROM users user WHERE user.firstName = 'Timber' AND user.lastName = 'Saw' in .where ("user.name IN (:...names)" , { names : [ "Timber" , "Cristal" , "Lina" ] }) 实际执行的sql语句:SELECT * FROM users user WHERE user.name IN ('Timber' , 'Cristal' , 'Lina' ) or .where ("user.firstName = :firstName" , { firstName : "Timber" }) .orWhere ("user.lastName = :lastName" , { lastName : "Saw" }); 实际执行的sql语句:SELECT * FROM users user WHERE user.firstName = 'Timber' OR user.lastName = 'Saw' 子句 const posts = await connection .getRepository (Post ) .createQueryBuilder ("post" ) .where (qb => { const subQuery = qb .subQuery () .select ("user.name" ) .from (User , "user" ) .where ("user.registered = :registered" ) .getQuery (); return "post.title IN " + subQuery; }) .setParameter ("registered" , true ) .getMany (); 实际执行的sql语句:select * from post where post.title in (select name from user where registered = true )
having筛选
.having ("user.firstName = :firstName" , { firstName : "Timber" }) .andHaving ("user.lastName = :lastName" , { lastName : "Saw" }); 实际执行的sql语句:SELECT ... FROM users user HAVING user.firstName = 'Timber' AND user.lastName = 'Saw'
orderBy排序
.orderBy ("user.name" , "DESC" ) .addOrderBy ("user.id" , "asc" ); 等于 .orderBy ({ "user.name" : "ASC" , "user.id" : "DESC" }); 实际执行的sql语句:SELECT * FROM users user order by user.name asc, user.id desc;
group分组
.groupBy ("user.name" ) .addGroupBy ("user.id" );
关系查询(多表)
1 参:你要加载的关系,2 参:可选,你为此表分配的别名,3 参:可选,查询条件左关联查询 .leftJoinAndSelect ("user.profile" , "profile" ) 右关联查询 .rightJoinAndSelect ("user.profile" , "profile" ) 内联查询 .innerJoinAndSelect ("user.photos" , "photo" , "photo.isRemoved = :isRemoved" , { isRemoved : false }) 例子: const result = await this .usersRepository .createQueryBuilder ('user' ) .leftJoinAndSelect ("user.photos" , "photo" ) .where ("user.name = :name" , { name : "joy" }) .andWhere ("photo.isRemoved = :isRemoved" , { isRemoved : false }) .getOne (); 实际执行的sql语句: SELECT user.*, photo.* FROM users userLEFT JOIN photos photo ON photo.user = user.id WHERE user.name = 'joy' AND photo.isRemoved = FALSE ;const result = await this .usersRepository .innerJoinAndSelect ("user.photos" , "photo" , "photo.isRemoved = :isRemoved" , { isRemoved : false }) .where ("user.name = :name" , { name : "Timber" }) .getOne (); 实际执行的sql语句: SELECT user.*, photo.* FROM users userINNER JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE WHERE user.name = 'Timber' ;多个关联 const result = await this .usersRepository .createQueryBuilder ("user" ) .leftJoinAndSelect ("user.profile" , "profile" ) .leftJoinAndSelect ("user.photos" , "photo" ) .leftJoinAndSelect ("user.videos" , "video" ) .getOne ();
typeorm使用事务的3种方式 typeorm使用事务的方式有哪些?如何使用?
事务
在操作多个表时,或者多个操作时,如果有一个操作失败,所有的操作都失败,要么全部成功,要么全部失
解决问题 :在多表操作时,因为各种异常导致一个成功,一个失败的数据错误。
例子:银行转账 如果用户1向用户2转了100元,但因为各种原因,用户2没有收到,如果没有事务处理,用户1扣除的100元就凭空消失了 如果有事务处理,只有用户2收到100元,用户1才会扣除100元,如果没有收到,则不会扣除。
应用场景
多表的增,删,改操作
nest-typrorm事务的使用方式
使用装饰器,在controller中编写,传递给service使用
使用getManager 或 getConnection,在service中编写与使用
使用connection 或 getConnection,开启queryRunner,在service中编写与使用
方式一:使用装饰器
controller
import { Controller , Post , Body , Param , ParseIntPipe , } from '@nestjs/common' ; import { Transaction , TransactionManager , EntityManager } from 'typeorm' ; 开启事务第一步:引入import { UserService } from './user.service-oto' ;@Controller ('user' ) export class UserController { constructor (private readonly userService: UserService ) { } @Post (':id' ) @Transaction () 开启事务第二步:装饰接口 async create ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this .userService .create (id, maneger); 开启事务第四步:传递给service,使用数据库时调用 } }
service
这里处理的是1对1关系:保存头像地址到avatar表,同时关联保存用户的id
如果你不会1对1关系,请先去学习对应的知识
import { Injectable } from '@nestjs/common' ;import { Repository , EntityManager } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { AvatarEntity } from './entities/avatar.entity' ;import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(AvatarEntity) private readonly avatarRepository: Repository<AvatarEntity>, ) { } async create (id: number, manager: EntityManager ) { const urlObj = { url : `http://www.dmyxs.com/images/${id} .png` , }; const user = await this .usersRepository .findOne ({ id }); 先查找用户,因为要保存用户的id if (!user) ToolsService .fail ('用户id不存在' ); 找不到用户抛出异常 const avatarEntity = this .avatarRepository .create ({ url : urlObj.url }); 创建头像地址的实体 const avatarUrl = await manager.save (avatarEntity); 使用事务保存副表 user.avatar = avatarUrl; 主表和副表建立关系 await manager.save (user); 使用事务保存主表 return '新增成功' ; 如果过程出错,不会保存 } }
方式二:使用getManager 或 getConnection
service
import { Injectable } from '@nestjs/common' ;import { Connection , Repository , getManager } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { AvatarEntity } from './entities/avatar.entity' ;import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, private readonly connection: Connection, ) { } async test (id: string ) { const urlObj = { url : `http://www.dmyxs.com/images/${id} .png` , }; const user = await this .usersRepository .findOne (id); 先查找用户 if (!user) ToolsService .fail ('用户id不存在' ); 找不到用户抛出异常 const result = await getManager ().transaction (async (manager) => { const avatarEntity = manager.create (AvatarEntity , { url : urlObj.url }); 创建头像地址的实体 const avatarUrl = await manager.save (AvatarEntity , avatarEntity); 使用事务保存副表 user.avatar = avatarUrl; 创建关联 return await manager.save (UsersEntity , user); 使用事务保存主表,并返回结果 }); return result; } } { "status" : 200 , "message" : "请求成功" , "data" : { "id" : 1 , "createdAt" : "2021-04-26T09:58:54.469Z" , "updatedAt" : "2021-04-28T14:47:36.000Z" , "deletedAt" : null , "username" : "admin" , "gender" : "male" , "age" : 18 , "status" : 1 , "avatar" : { "url" : "http://www.dmyxs.com/images/1.png" , "id" : 52 } } }
方式三:使用 connection 或 getConnection
service
import { Injectable } from '@nestjs/common' ;import { Connection , Repository , getManager } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { AvatarEntity } from './entities/avatar.entity' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, private readonly connection: Connection, ) { } async test (id: string ) { const urlObj = { url : `http://www.test.com/images/${id} .png` , }; const user = await this .usersRepository .findOne (id); 先查找用户 if (!user) ToolsService .fail ('用户id不存在' ); 找不到用户抛出异常 const queryRunner = this .connection .createQueryRunner (); 获取连接并创建新的queryRunner await queryRunner.connect (); 使用我们的新queryRunner建立真正的数据库连 await queryRunner.startTransaction (); 开始事务 const avatarEntity = new AvatarEntity (); 创建实体:要保存的数据 avatarEntity.url = urlObj.url ; try { const result = await queryRunner.manager 使用事务保存到副表 .getRepository (AvatarEntity ) .save (avatarEntity); user.avatar = result; 主表和副表建立连接 const userResult = await queryRunner.manager 使用事务保存到副表 .getRepository (UsersEntity ) .save (user); await queryRunner.commitTransaction (); 提交事务 return userResult; 返回结果 } catch (error) { console .log ('创建失败,取消事务' ); await queryRunner.rollbackTransaction (); 出错回滚 } finally { await queryRunner.release (); 释放 } } }
typeorm 一对一关系设计与增删改查 实体如何设计一对一关系?如何增删改查?
一对一关系
定义:一对一是一种 A 只包含一个 B ,而 B 只包含一个 A 的关系
其实就是要设计两个表:一张是主表,一张是副表,查找主表时,关联查找副表
有外键的表称之为副表,不带外键的表称之为主表
如:一个账户对应一个用户信息,主表是账户,副表是用户信息
如:一个用户对应一张用户头像图片,主表是用户信息,副表是头像地址
一对一实体设计
主表:
使用@OneToOne() 来建立关系
第一个参数:() => AvatarEntity, 和谁建立关系? 和AvatarEntity建立关系
第二个参数:(avatar) => avatar.user),和哪个字段联立关系? avatar就是AvatarEntity的别名,可随便写,和AvatarEntity的userinfo字段建立关系
第三个参数:RelationOptions关系选项
import { Column , Entity , PrimaryGeneratedColumn , OneToOne , } from 'typeorm' ; import { AvatarEntity } from './avatar.entity' ;@Entity ({ name : 'users' }) export class UsersEntity { @PrimaryGeneratedColumn () id : number; @Column () username : string; @Column () password : string; @OneToOne (() => AvatarEntity , (avatar ) => avatar.userinfo ) avatar : AvatarEntity ; }
副表
参数:同主表一样 主要:根据@JoinColumn({ name: ‘user_id’ })来分辨副表,name是设置数据库的外键名字,如果不设置是userId
import { Entity , PrimaryGeneratedColumn , Column , OneToOne , JoinColumn , } from 'typeorm' ; import { UsersEntity } from './user.entity' ;@Entity ({ name : 'avatar' }) export class AvatarEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' }) url : string; @OneToOne (() => UsersEntity , (user ) => user.avatar ) @JoinColumn ({ name : 'userinfo_id' }) userinfo : UsersEntity ; }
一对一增删改查
注意 :只要涉及两种表操作的,就需要开启事务:同时失败或同时成功,避免数据不统一
在这里 :创建,修改,删除都开启了事务
注意 :所有数据应该是由前端传递过来的,这里为了方便,直接硬编码了(写死)
import { Controller , Get , Post , Body , Patch , Query , Param , Delete , HttpCode , HttpStatus , ParseIntPipe , } from '@nestjs/common' ; import { Transaction , TransactionManager , EntityManager } from 'typeorm' ; 开启事务第一步:引入import { UserService } from './user.service-oto' ;@Controller ('user' ) export class UserController { constructor (private readonly userService: UserService ) { } @Get () async findAll ( ) { const [data, count] = await this .userService .findAll (); return { count, data }; } @Get (':id' ) async findOne (@Param('id' , new ParseIntPipe()) id: number ) { return await this .userService .findOne (id); } @Post (':id' ) @HttpCode (HttpStatus .OK ) @Transaction () 开启事务第二步:装饰接口 async create ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this .userService .create (id, maneger); 开启事务第四步:传递给service,使用数据库时调用 } @Patch (':id' ) @Transaction () async update ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this .userService .update (id, maneger); } @Delete (':id' ) @Transaction () async remove ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this .userService .remove (id, maneger); } }
import { Injectable } from '@nestjs/common' ;import { Repository , Connection , UpdateResult , EntityManager } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { AvatarEntity } from './entities/avatar.entity' ;import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(AvatarEntity) private readonly avatarRepository: Repository<AvatarEntity>, private connection: Connection, ) { } 一对一增删改查 查找全部 async findAll ( ) { 使用封装好的方式 使用QueryBuilder 的方式 const list = await this .usersRepository .createQueryBuilder ('UsersEntity' ) .leftJoinAndSelect ('UsersEntity.avatar' , 'AvatarEntity.userinfo' ) .getManyAndCount (); return list; } 根据主表id查找一对一 async findOne (id: number ) { const result = await this .usersRepository .findOne (id, { relations : ['avatar' ], }); if (!result) ToolsService .fail ('用户id不存在' ); return result; } 根据主表id创建一对一 async create (id: number, manager: EntityManager ) { const urlObj = { url : `http://www.dmyxs.com/images/${id} .png` , }; const user = await this .usersRepository .findOne ({ id }); 先查找用户 if (!user) ToolsService .fail ('用户id不存在' ); 如果没找到,抛出错误,由过滤器捕获错误 创建实体的两种方式:new 和 create,new 的方式方便条件判断 创建实体方式一: const avatarEntity = this .avatarRepository .create ({ url : urlObj.url }); 创建实体 创建实体方式二: const avatarUrl = await manager.save (avatarEntity); 使用事务保存副表 user.avatar = avatarUrl; 主表和副表建立关系 await manager.save (user); 使用事务保存主表 return '新增成功' ; 如果过程出错,不会保存 } 根据主表id更改一对一 要更改的副表id,会从前端传递过来 async update (id: number, manager: EntityManager ) { const urlObj = { id : 18 , url : `http://www.dmyxs.com/images/${id} -update.jpg` , }; const user = await this .usersRepository .findOne ( { id } ); 先查找用户 if (!user) ToolsService .fail ('用户id不存在' ); 如果没找到id抛出错误,由过滤器捕获错误 const avatarEntity = this .avatarRepository .create ({ url : urlObj.url }); 创建要修改的实体 使用事务更新方法:1 参:要修改的表,2 参:要修改的id, 3 参:要更新的数据 await manager.update (AvatarEntity , urlObj.id , avatarEntity); return '更新成功' ; } 根据主表id删除一对一 async remove (id : number, manager : EntityManager ): Promise <any> { const user = await this .usersRepository .findOne (id); if (!user) ToolsService .fail ('用户id不存在' ); 只删副表的关联数据 await manager.delete (AvatarEntity , { user : id }); 如果连主表用户一起删,加下面这行代码 return '删除成功' ; } }
typeorm 一对多和多对一关系设计与增删改查 实体如何设计一对多与多对一关系,如何关联查询
一对多关系,多对一关系
定义:一对多是一种一个 A 包含多个 B ,而多个B只属于一个 A 的关系 其实就是要设计两个表:一张是主表(一对多),一张是副表(多对一),查找主表时,关联查找副表 有外键的表称之为副表,不带外键的表称之为主表 如:一个用户拥有多个宠物,多个宠物只属于一个用户的(每个宠物只能有一个主人) 如:一个用户拥有多张照片,多张照片只属于一个用户的 如:一个角色拥有多个用户,多个用户只属于一个角色的(每个用户只能有一个角色)
一对多和多对一实体设计
一对多
使用@OneToMany() 来建立一对多关系 第一个参数:() => PhotoEntity, 和谁建立关系? 和PhotoEntity建立关系 第二个参数:(user) => user.photo,和哪个字段联立关系? user就是PhotoEntity的别名,可随便写,和PhotoEntity的userinfo字段建立关系 第三个参数:RelationOptions关系选项
import { Column , Entity , PrimaryGeneratedColumn , OneToOne , } from 'typeorm' ; import { AvatarEntity } from './avatar.entity' ;@Entity ({ name : 'users' }) export class UsersEntity { @PrimaryGeneratedColumn () id : number; @Column () username : string; @Column () password : string; @OneToMany (() => PhotoEntity , (avatar ) => avatar.userinfo ) photos : PhotoEntity ; }
多对一
使用@ManyToOne() 来建立多对一关系,参数如同上
import { Entity , PrimaryGeneratedColumn , Column , ManyToOne } from 'typeorm' ;import { UsersEntity } from './user.entity' ;@Entity ({ name : 'photo' }) export class PhotoEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' }) url : string; @ManyToOne (() => UsersEntity , (user ) => user.photos ) @JoinColumn ({ name : 'userinfo_id' }) userinfo : UsersEntity ; }
一对多和多对一增删改查
只要涉及两种表操作的,就需要开启事务:同时失败或同时成功,避免数据不统一 注意:所有数据应该是由前端传递过来的,这里为了方便,直接硬编码了(写死) 比较复杂的是更新操作
user.controller.ts
import { Controller , Get , Post , Body , Patch , Query , Param , Delete , HttpCode , HttpStatus , ParseIntPipe , } from '@nestjs/common' ; import { Transaction , TransactionManager , EntityManager } from 'typeorm' ; 开启事务第一步:引入import { UserService } from './user.service-oto' ;@Controller ('user' ) export class UserController { constructor (private readonly userService: UserService ) { } @Get () async findAll ( ) { const [data, count] = await this .userService .findAll (); return { count, data }; } @Get (':id' ) async findOne (@Param('id' , new ParseIntPipe()) id: number ) { return await this .userService .findOne (id); } @Post (':id' ) @HttpCode (HttpStatus .OK ) @Transaction () 开启事务第二步:装饰接口 async create ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this .userService .create (id, maneger); 开启事务第四步:传递给service,使用数据库时调用 } @Patch (':id' ) @Transaction () async update ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this .userService .update (id, maneger); } @Delete (':id' ) @Transaction () async remove ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this .userService .remove (id, maneger); } }
user.service.ts
令人头大的地方 :建立关系和查找使用实体,删除使用实体的id,感觉设计得不是很合理,违背人的常识
import { Injectable } from '@nestjs/common' ;import { Repository , EntityManager } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { PhotoEntity } from './entities/photo.entity' ;import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(PhotoEntity) private readonly photoRepository: Repository<PhotoEntity>, ) { } 一对多增删改查 async findAll ( ) { const list = await this .usersRepository .createQueryBuilder ('UsersEntity' ) .leftJoinAndSelect ('UsersEntity.photos' , 'PhotoEntity.userinfo' ) .getManyAndCount (); return list; } 根据主表id查找一对多 async findOne (id: number ) { 查询一个用户有多少张照片(一对多) const result = await this .usersRepository .findOne (id, { relations : ['photos' ], }); if (!result) ToolsService .fail ('用户id不存在' ); return result; 查询这张照片属于谁(多对一) } 根据主表id创建一对多 async create (id: number, manager: EntityManager ) { const urlList = [ { url : `http://www.dmyxs.com/images/${id} .png` , }, { url : `http://www.dmyxs.com/images/${id} .jpg` , }, ]; const user = await this .usersRepository .findOne ({ id }); if (!user) ToolsService .fail ('用户id不存在' ); 遍历传递过来的数据 if (urlList.length !== 0 ) { for (let i = 0 ; i < urlList.length ; i++) { 创建实体的两种方式:new 和 create,new 的方式方便条件判断 const photoEntity = this .photoRepository .create ({ url : urlList[i].url , userinfo : user, 注意:这里是使用实体建立关系,而不是实体id }); await manager.save (photoEntity); } } return '新增成功' ; } 根据主表id更改一对多 示例:删除一张,修改一张(修改的有id),新增一张 先使用创建,创建两张photo async update (id: number, manager: EntityManager ) { const urlList = [ { id : 22 , url : `http://www.dmyxs.com/images/${id} -update.png` , }, { url : `http://www.dmyxs.com/images/${id} -create.jpeg` , }, ]; const user = await this .usersRepository .findOne ({ id }); if (!user) ToolsService .fail ('用户id不存在' ); 如果要修改主表,先修改主表用户信息,后修改副表图片信息 修改主表 const userEntity = this .usersRepository .create ({ id, username : 'admin7' , password : '123456' , }); await manager.save (userEntity); 修改副表 如果前端附带了图片list if (urlList.length !== 0 ) { 查询数据库已经有的图片 const databasePhotos = await manager.find (PhotoEntity , { userinfo : user }); 如果有数据,则进行循环判断,先删除多余的数据 if (databasePhotos.length >= 1 ) { for (let i = 0 ; i < databasePhotos.length ; i++) { 以用户传递的图片为基准,数据库的图片id是否在用户传递过来的表里,如果不在,就是要删除的数据 const exist = urlList.find ((item ) => item.id === databasePhotos[i].id ); if (!exist) { await manager.delete (PhotoEntity , { id : databasePhotos[i].id }); } } } 否则就是新增和更改的数据 for (let i = 0 ; i < urlList.length ; i++) { const photoEntity = new PhotoEntity (); photoEntity.url = urlList[i].url ; 如果有id则是修改操作,因为前端传递的数据是从服务端获取的,会附带id,新增的没有 if (!!urlList[i].id ) { 修改则让id关联即可 photoEntity.id = urlList[i].id ; await manager.save (PhotoEntity , photoEntity); } else { 否则是新增操作,关联用户实体 photoEntity.userinfo = user; await manager.save (PhotoEntity , photoEntity); } } } else { 如果前端把图片全部删除,删除所有关联的图片 await manager.delete (PhotoEntity , { userinfo : id }); } return '更新成功' ; } 根据主表id删除一对多 async remove (id : number, manager : EntityManager ): Promise <any> { const user = await this .usersRepository .findOne (id); if (!user) ToolsService .fail ('用户id不存在' ); 只删副表的关联数据 await manager.delete (PhotoEntity , { userinfo : id }); 如果连主表用户一起删,加下面这行代码 return '删除成功' ; } }
typeorm 多对多关系设计与增删改查
实体如何设计多对多关系?如何增删改查?
多对多关系
定义:多对多是一种 A 包含多个 B,而 B 包含多个 A 的关系 如:一个粉丝可以关注多个主播,一个主播可以有多个粉丝 如:一篇文章属于多个分类,一个分类下有多篇文章 比如这篇文章,可以放在nest目录,也可以放在typeorm目录或者mysql目录
实现方式
第一种:建立两张表,使用装饰器@ManyToMany建立关系,typeorm会自动生成三张表 第二种:手动建立3张表
这里使用第一种
实体设计 这里将设计一个用户(粉丝) 与 明星的 多对多关系
用户(粉丝)可以主动关注明星,让users变为主表,加入@JoinTable()
使用@ManyToMany() 来建立多对多关系 第一个参数:() => StarEntity, 和谁建立关系? 和StarEntity建立关系 第二个参数:(star) => star.photo,和哪个字段联立关系? star就是StarEntity的别名,可随便写,和PhotoEntity的followers字段建立关系
用户(粉丝)表:follows关注/跟随
import { Column , Entity , PrimaryGeneratedColumn , ManyToMany , JoinTable , } from 'typeorm' ; import { AvatarEntity } from './avatar.entity' ;@Entity ({ name : 'users' }) export class UsersEntity { @PrimaryGeneratedColumn () id : number; @Column () username : string; @Column () password : string; @ManyToMany (() => StarEntity , (star ) => star.followers ) @JoinTable () follows : StarEntity []; 注意这里是数组类型 }
明星表:followers跟随者
import { Entity , PrimaryGeneratedColumn , Column , ManyToMany } from 'typeorm' ;import { UsersEntity } from './user.entity' ;@Entity ({ name : 'star' }) export class StarEntity { @PrimaryGeneratedColumn () id : number; @Column ({ type : 'varchar' }) name : string; @ManyToMany (() => UsersEntity , (user ) => user.follows ) followers : UsersEntity ; }
注意:
程序运行后,将会默认在数据库中生成三张表,users,star,users_follows_star,users_follows_star是中间表,用于记录users和star之间的多对多关系,它是自动生成的。
为了测试方便,你可以在users表和star表创建一些数据:这些属于单表操作
多对多增删改查
只要涉及两种表操作的,就需要开启事务:同时失败或同时成功,避免数据不统一 注意:所有数据应该是由前端传递过来的,这里为了方便,直接硬编码了(写死)
user.controller.ts
import { Controller , Get , Post , Body , Patch , Query , Param , Delete , HttpCode , HttpStatus , ParseIntPipe , } from '@nestjs/common' ; import { Transaction , TransactionManager , EntityManager } from 'typeorm' ; 开启事务第一步:引入import { UserService } from './user.service-oto' ;@Controller ('user' ) export class UserController { constructor (private readonly userService: UserService ) { } @Get () async findAll ( ) { const [data, count] = await this .userService .findAll (); return { count, data }; } @Get (':id' ) async findOne (@Param('id' , new ParseIntPipe()) id: number ) { return await this .userService .findOne (id); } @Post (':id' ) @HttpCode (HttpStatus .OK ) @Transaction () 开启事务第二步:装饰接口 async create ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, 开启事务第三步:获取事务管理器 ) { return await this .userService .create (id, maneger); 开启事务第四步:传递给service,使用数据库时调用 } @Patch (':id' ) @Transaction () async update ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this .userService .update (id, maneger); } @Delete (':id' ) @Transaction () async remove ( @Param('id' , new ParseIntPipe()) id: number, @TransactionManager() maneger: EntityManager, ) { return await this .userService .remove (id, maneger); } }
user.service.ts
import { Injectable } from '@nestjs/common' ;import { Repository , EntityManager } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { UsersEntity } from './entities/user.entity' ;import { StarEntity } from './entities/star.entity' ;import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class UserService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, @InjectRepository(StarEntity) private readonly starRepository: Repository<StarEntity>, ) { } 一对多增删改查 async findAll ( ) { const list = await this .usersRepository .createQueryBuilder ('UsersEntity' ) .leftJoinAndSelect ('UsersEntity.follows' , 'StarEntity.followers' ) .getManyAndCount (); return list; } 根据主表id查找多对多 async findOne (id: number ) { 查询一个用户关注了哪些明星 查询一个明星有多少粉丝 const result = await this .starRepository .findOne (id, { relations : ['followers' ], }); if (!result) ToolsService .fail ('明星id不存在' ); return result; } 根据主表id创建多对多 粉丝关注明星 async create (id: number, manager: EntityManager ) { 要关注的明星id数组 const willFollow = [3 , 4 ]; const user = await this .usersRepository .findOne ({ id }); if (!user) ToolsService .fail ('用户id不存在' ); if (willFollow.length !== 0 ) { const followList = []; for (let i = 0 ; i < willFollow.length ; i++) { const star = await manager.findOne (StarEntity , { id : willFollow[i], }); if (!star) ToolsService .fail ('主播id不存在' ); followList.push (star); } const userEntity = new UsersEntity (); 重点: 不指定id是创建新的用户,还需要填写username和password等必填的字段 指定id就是更新某些字段:只关注明星,不创建新的用户,同样可用于修改 userEntity.id = id; userEntity.follows = followList; 建立关联,数据表会自动更新 await manager.save (userEntity); } return '新增成功' ; } 根据主表id更改多对多 假设:某用户关注了id为[3 , 4 ]的明星, 现在修改为只关注[2 ] 逻辑和创建一样 async update (id: number, manager: EntityManager ) { const willFollow = [2 ]; const user = await this .usersRepository .findOne ({ id }); if (!user) ToolsService .fail ('用户id不存在' ); if (willFollow.length !== 0 ) { const followList = []; for (let i = 0 ; i < willFollow.length ; i++) { const listOne = await manager.findOne (StarEntity , { id : willFollow[i], }); if (!listOne) ToolsService .fail ('主播id不存在' ); followList.push (listOne); } const userEntity = new UsersEntity (); userEntity.id = id; userEntity.follows = followList; await manager.save (userEntity); } return '更新成功' ; } 根据主表id删除多对多 多种删除 async remove (id : number, manager : EntityManager ): Promise <any> { const user = await this .usersRepository .findOne (id, { relations : ['follows' ], }); if (!user) ToolsService .fail ('用户id不存在' ); 根据id删除一个:取消关注某个明星,明星id应由前端传递过来,这里写死 需要获取当前用户的的follows,使用关系查询 const willDeleteId = 2 ; if (user.follows .length !== 0 ) { 过滤掉要删除的数据,再重新赋值 const followList = user.follows .filter ((star ) => star.id != willDeleteId); const userEntity = new UsersEntity (); userEntity.id = id; userEntity.follows = followList; await manager.save (userEntity); } 全部删除关联数据,不删用户 如果连用户一起删,会将关联数据一起删除 return '删除成功' ; } }
nest连接Redis
Redis 字符串数据类型的相关命令用于管理 redis 字符串值
查看所有的key: keys *
普通设置: set key value
设置并加过期时间:set key value EX 30 表示30秒后过期
获取数据: get key
删除指定数据:del key
删除全部数据: flushall
查看类型:type key
设置过期时间: expire key 20 表示指定的key5秒后过期
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
列表右侧增加值:rpush key value
列表左侧增加值:lpush key value
右侧删除值:rpop key
左侧删除值: lpop key
获取数据: lrange key
删除指定数据:del key
删除全部数据: flushall
查看类型: type key
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。它和列表的最主要区别就是没法增加重复值
给集合增数据:sadd key value
删除集合中的一个值:srem key value
获取数据:smembers key
删除指定数据: del key
删除全部数据: flushall
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
设置值hmset :hmset zhangsan name "张三" age 20 sex “男”
设置值hset : hset zhangsan name "张三"
获取数据:hgetall key
删除指定数据:del key
删除全部数据: flushall
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
client.publish ('publish' , 'message from publish.js' ); client.subscribe ('publish' ); client.on ('message' , function (channel, msg ){ console .log ('client.on message, channel:' , channel, ' message:' , msg);});
Nestjs中使用redis
Nestjs Redis 官方文档:https://github.com/kyknow/nestjs-redis
npm install nestjs-redis --save
如果是nest8需要注意该问题:https://github.com/skunight/nestjs-redis/issues/82
import { RedisModule } from 'nestjs-redis' ;import { RedisTestModule } from '../examples/redis-test/redis-test.module' ;@Module ({ imports : [ ConfigModule .load (resolve (__dirname, 'config' , '**/!(*.d).{ts,js}' )), RedisModule .forRootAsync ({ useFactory : (configService: ConfigService ) => configService.get ('redis' ), inject : [ConfigService ], }), RedisTestModule , ], controllers : [], providers : [ ], }) export class AppModule implements NestModule {}
export default { host : '127.0.0.1' , port : 6379 , db : 0 , password : '' , keyPrefix : '' , onClientReady : (client ) => { client.on ('error' , (err ) => { console .log ('-----redis error-----' , err); }); }, };
创建一个cache.service.ts 服务 封装操作redis的方法
import { Injectable } from '@nestjs/common' ;import { RedisService } from 'nestjs-redis' ;@Injectable () export class CacheService { public client; constructor (private redisService: RedisService ) { this .getClient (); } async getClient ( ) { this .client = await this .redisService .getClient (); } async set (key: string, value: any, seconds?: number ) { value = JSON .stringify (value); if (!this .client ) { await this .getClient (); } if (!seconds) { await this .client .set (key, value); } else { await this .client .set (key, value, 'EX' , seconds); } } async get (key: string ) { if (!this .client ) { await this .getClient (); } const data = await this .client .get (key); if (!data) return ; return JSON .parse (data); } async del (key : string): Promise <any> { if (!this .client ) { await this .getClient (); } await this .client .del (key); } async flushall (): Promise <any> { if (!this .client ) { await this .getClient (); } await this .client .flushall (); } }
使用redis服务
redis-test.controller
import { Body , Controller , Get , Post , Query } from '@nestjs/common' ;import { CacheService } from 'src/common/cache/redis.service' ;@Controller ('redis-test' ) export class RedisTestController { constructor (private readonly cacheService: CacheService ) {} @Get ('get' ) async get (@Query() query ) { return await this .cacheService .get (query.key ); } @Post ('set' ) async set (@Body() body ) { const { key, ...params } = body as any; return await this .cacheService .set (key, params); } @Get ('del' ) async del (@Query() query ) { return await this .cacheService .del (query.key ); } @Get ('delAll' ) async delAll ( ) { return await this .cacheService .flushall (); } }
redis-test.module.ts
import { Module } from '@nestjs/common' ;import { RedisTestService } from './redis-test.service' ;import { RedisTestController } from './redis-test.controller' ;import { CacheService } from 'src/common/cache/redis.service' ;@Module ({ controllers : [RedisTestController ], providers : [RedisTestService , CacheService ], }) export class RedisTestModule {}
redis-test.service.ts
import { Injectable } from '@nestjs/common' ;@Injectable () export class RedisTestService {}
集成redis实现单点登录 在要使用的controller或service中使用redis
单点登陆原理
import { Injectable } from '@nestjs/common' ;import { JwtService } from '@nestjs/jwt' ;import { Repository } from 'typeorm' ;import { InjectRepository } from '@nestjs/typeorm' ;import { compareSync, hashSync } from 'bcryptjs' ;import { UsersEntity } from '../user/entities/user.entity' ;import { ToolsService } from '../../utils/tools.service' ;import { CreateUserDto } from '../user/dto/create-user.dto' ;import { CacheService } from '../../common/db/redis-ceche.service' ;@Injectable () export class AuthService { constructor ( @InjectRepository(UsersEntity) private readonly usersRepository: Repository<UsersEntity>, private readonly jwtService: JwtService, private readonly redisService: CacheService, ) { } async create (user: CreateUserDto ) { const { username, password } = user; const transformPass = hashSync (password); user.password = transformPass; const result = await this .usersRepository .findOne ({ username }); if (result) ToolsService .fail ('用户名已存在' ); return await this .usersRepository .insert (user); } async validateUser (userinfo): Promise <any> { const { username, password } = userinfo; const user = await this .usersRepository .findOne ({ where : { username }, select : ['username' , 'password' , 'id' ], }); if (!user) ToolsService .fail ('用户名或密码不正确' ); if (!compareSync (password, user.password )) { ToolsService .fail ('用户名或密码不正确' ); } return user; } async login (user : any): Promise <any> { const { id, username } = user; const payload = { id, username }; const access_token = this .jwtService .sign (payload); await this .redisService .set (`user-token-${id} ` , access_token, 60 * 60 * 24 ); 在这里使用redis return access_token; } }
验证token
import { Strategy , ExtractJwt , StrategyOptions } from 'passport-jwt' ;import { Injectable } from '@nestjs/common' ;import { PassportStrategy } from '@nestjs/passport' ;import { jwtConstants } from './constants' ;import { CacheService } from '../../common/db/redis-ceche.service' ;import { Request } from 'express' ;import { ToolsService } from '../../utils/tools.service' ;@Injectable () export class JwtStrategy extends PassportStrategy (Strategy ) { constructor (private redisService: CacheService ) { super ({ jwtFromRequest : ExtractJwt .fromHeader ('token' ), ignoreExpiration : false , secretOrKey : jwtConstants.secret , passReqToCallback : true , } as StrategyOptions ); } async validate (req: Request, payload: any ) { console .log ('payload' , payload); const { id } = payload; const token = ExtractJwt .fromHeader ('token' )(req); const cacheToken = await this .redisService .get (`user-token-${id} ` ); 获取redis的key if (token !== JSON .parse (cacheToken)) { ToolsService .fail ('您账户已经在另一处登陆,请重新登陆' , 401 ); } return { username : payload.username }; } }
QA Q:nestJS注入其他依赖时为什么还需要导入其module
A模块的Service需要调用B模块的service中一个方法,则需要在A的Service导入B的service 场景如下:
import { BService } from '../B/B.service' ;@Injectable () export class A { constructor ( private readonly _BService: BService, ) {}}
我的理解
在此处@Injectable装饰器已经将B的Service类实例化了,
已经可以使用B的类方法了。
但为什么还需要在A的module.ts中导入B模块呢?像是这样:
import { BModule } from '../B/B.module' ;@Module ({ imports : [BModule ], controllers : [AController ], providers : [AService ], exports : [AService ], }) export class AModule {}
A
为啥”为什么还需要在A的module.ts中导入B模块呢”?
因为 BService的作用域只在 BModule里,所以你要在 AController里直接用,就会报错拿不到实例。
再来说,”有什么办法可以让 BService随处直接用么?”,参考如下手段:
B 的module 声明时,加上@Global,如下:
import { Module , Global } from '@nestjs/common' ;import { BService } from './B.service' ;@Global () @Module ({ providers : [BService ], exports : [BService ], }) export class BModule {}
这样,你就不用在 AModule的声明里引入 BModule了。
关于『你的理解』部分,貌似你把@Inject 和 @Injectable 搞混了,建议再读一读这个部分的文档,多做些练习/尝试,自己感受下每个api的特点。
最后,官网文档里其实有介绍 ,看依赖注入:https://docs.nestjs.com/modules#dependency-injection