【NestJS系列】连接数据库及优雅地处理响应
前言
Nest作为一个node框架,当然也可以连接数据库,为前端提供CURD接口
我们以mysql为例,自行安装mysql
TypeORM
TypeORM 是一个ORM框架,它可以运行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron 平台上,可以与 TypeScript 和 JavaScript一起使用。它的目标是始终支持最新的 JavaScript 特性并提供额外的特性以帮助你开发任何使用数据库的(不管是只有几张表的小型应用还是拥有多数据库的大型企业应用)应用程序。
TypeORM作为TypeScript中最成熟的对象关系映射器,可以很好的与Nest框架集成使用。
安装依赖
npm install --save @nestjs/typeorm typeorm mysql2
新建数据库
CREATE DATABASE nanjiu DEFAULT CHARACTER SET = 'utf8mb4';
新建一个nanjiu数据库
图片
连接数据库
数据库建好之后,我们就可以使用typeorm来连接数据库并建立映射关系了
// dbConfig.ts// 数据库配置export function dbConfig() { return { type: 'mysql', // 数据库类型 host: '127.0.0.1', // 数据库地址 port: 3306, // 端口 username: 'root', // 用户名 password: '123456', // 密码 database: 'nanjiu', // 数据库名 entities: [__dirname + '/../**/*.entity{.ts,.js}'], // 实体类 synchronize: true, // 自动创建表 autoLoadEntities: true, // 自动加载实体类 } as DbConfig}
需要在app.module.ts中进行注册
@Module({ imports: [ NanjiuModule, UserModule, InfoModule, TypeOrmModule.forRoot(dbConfig() as any) ], controllers: [AppController], providers: [AppService],})
图片
定义实体
实体是一个映射到数据库表的类,使用@Entity装饰器来定义
// user.entry.tsimport { Column, Entity, PrimaryGeneratedColumn } from "typeorm";@Entity('user') // 表名export class User { @PrimaryGeneratedColumn() // 自增主键 id: number; @Column() // 字段 name: string;}
图片
基本实体由列和关系组成,每个实体必须有一个主列。
每个实体都必须在连接配置中注册:
entities: [__dirname + '/../**/*.entity{.ts,.js}'], // 实体类
关联实体
实体定义后需要在module中导入并关联
@Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UserController], providers: [UserService]})
图片
当你做完这一步之后你会发现数据库里已经根据你刚刚定义的实体建好了表
图片
这是因为刚刚数据库配置那里开启了synchronize: true 自动创建表
CURD接口
数据库准备准备工作完成后,我们就可以来写接口了
「在controller控制器中定义接口path」
// user.controller.tsimport { CreateUserDto } from './dto/create-user.dto';export class UserController { constructor( private readonly userService: UserService, ) {} @Post('addUser') create(@Body() createUserDto: CreateUserDto) { // 添加用户 return this.userService.add(createUserDto); }}
「新建DTO数据验证器」
import { Injectable } from "@nestjs/common";import { IsNotEmpty, IsString } from "class-validator"; // 引入验证器@Injectable() export class CreateUserDto { @IsString({ message: '用户名必须是字符串'}) // 验证是否是字符串 @IsNotEmpty({ message: '用户名不能为空'}) // 验证是否为空 name: string; // 用户名}
dto一般用来做参数验证
「注册全局DTO验证管道」
// main.tsimport { ValidationPipe } from '@nestjs/common';app.useGlobalPipes(new ValidationPipe()) // 全局验证管道
「service逻辑处理,入库操作」
// user.service.tsimport { Injectable } from '@nestjs/common';import { CreateUserDto } from './dto/create-user.dto';import { User } from './entities/user.entity';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';@Injectable()export class UserService { constructor( // 使用 @InjectRepository(User) 注入实数据库实体 @InjectRepository(User) private readonly userRepository: Repository ) {} async add(createUserDto: CreateUserDto) { // 添加用户,更多操作参考 TypeORM 文档 const res = await this.userRepository.save(createUserDto); return res }}
「调用接口」
图片
「查看数据库」
调用完接口,此时数据库中会新增一条数据
图片
响应结果处理
从上面的响应结果来看并不规范,只是简单的返回了数据库查询结果,并且当系统发生异常错误时,如果我们没有手动处理异常,所有的异常都会进入到nest内置的异常处理层,它返回的信息格式如下:
{ "statusCode": 500, "message": "Internal server error"}
比如我们往user库中插入相同的name,但name设置了唯一性,所以这时会抛出错误,如果我们不处理返回给前端就是上面那种信息,这样前端同学看到就会很蒙,根本不知道为啥报错
图片
所以我们要做的就是将响应格式化处理
在nest中,一般是在「service」中处理异常,如果有异常,直接抛出错误,由「过滤器」捕获,统一格式返回,如果成功,service把结果返回,controller直接return结果即可,由「拦截器」捕获,统一格式返回 失败:过滤器统一处理 成功:拦截器统一处理
异常拦截器
为了更加优雅地处理异常,我们可以创建一个异常过滤器,它主要用来捕获作为HttpException类实例的异常。
「异常抛出封装:」
// httpStatus.service.tsimport { Injectable, HttpException, HttpStatus, NestInterceptor } from '@nestjs/common'@Injectable()export class HttpStatusError { static fail(error, status = HttpStatus.BAD_REQUEST) { throw new HttpException({statusCode: status, message: '请求失败', error}, status) }}
「抛出异常:」
// group.service.ts// ...import { HttpStatusError } from '../utils/httpStatus.service'@Injectable()export class GroupService { constructor( @InjectRepository(Group) private groupRepository: Repository, @InjectRepository(Template) private templateRepository: Repository, ) {} // todo: 添加分组 async create(createGroupDto: CreateGroupDto) { const data = this.groupRepository.create(createGroupDto); const group = await this.groupRepository.findOne({ where: { name: createGroupDto.name } }); if (group) { return HttpStatusError.fail('该分组已存在'); } try { const res = await this.groupRepository.save(data, { reload: true }); return res; } catch (error) { return HttpStatusError.fail(error); } }}
「异常拦截器封装:」
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().toLocaleString(), }; response.status(status).json(msgLog); } }
「使用:」
app.useGlobalFilters(new HttpExceptionFilter()); // 全局异常过滤器
「请求:」
图片
这样报错信息就能够一目了然,简单实用的话可以直接抛出异常就可以,然后在抛出异常的地方给出详细信息。
全局响应拦截器
那成功的响应应该如何优雅地处理呢?
「Interceptor拦截器」
这里我们可以使用Interceptor拦截器,给成功响应按固定格式返回
import { Injectable, HttpException, HttpStatus, NestInterceptor, ExecutionContext,CallHandler } from '@nestjs/common'import { Observable } from 'rxjs'import { map } from 'rxjs/operators'@Injectable()export class HttpStatusSuccess implements NestInterceptor{ intercept(context: ExecutionContext, next: CallHandler) :Observable { return next.handle().pipe(map(data => { return { statusCode: HttpStatus.OK, message: '请求成功', data } })) }}
「使用:」
app.useGlobalInterceptors(new HttpStatusSuccess()); // 全局拦截器请求成功
「请求:」
图片
推荐阅读
-
我们如何从现有 MySQL 表的列中删除 FOREIGN KEY 约束?
我们可以通过使用DROP关键字和ALTERTABLE语句从现有表的列中删除FOREIGNKEY约束。语法ALTER...
-
如何改变MySQL表的列位置而不丢失列数据?
借助ALTERTABLE命令,您可以更改MySQL表的列位置而不会丢失数据。语法如下–ALTERTABLEyour...
-
在 MongoDB 中存储日期/时间的最佳方式?
可以通过两种不同的方式在MongoDB中存储日期/时间。在第一种方法中,您可以像JavaScript一样使用Date对...
-
修复 MySQL 数据库错误 #1064?
mysql˃createtableDemoTable(UserIdintNOTNULLAUTO_I...
-
如何在任何 MySQL 表中实现 CANDIDATE 键?
每个关系可能有一个或多个候选键。这些候选键之一称为主键。每个候选键都有资格成为主键。因此,候选主键称为候选键。要在MySQL中...
-
我们如何创建一个在某个指定时间间隔后执行的 MySQL 一次性事件?
示例mysql˃CREATEEVENTtesting_event5ONSCHEDULEATCURRENT_TIMES...
-
如何统计MySQL数据库中表的数量?
要计算表的总数,请使用table_schema的count(*)概念。首先,要检查我们的数据库“business”中有多少...
-
MySQL 如何使用 YEAR 数据类型在表中存储年份值?
MySQL允许声明列YEAR类型,借助它我们可以在该列中存储年份值。mysql˃Createtableyear1(Y...
-
MySQL 和 SQL Server 的区别
MySQL和SQLServer都是关系数据库管理系统或RDBMS。MySQL是开源的,可以免费使用,而SQLSe...
-
如何向现有 MySQL 表添加列?
通过使用ALTER命令,我们可以向现有表添加列。Altertabletable-nameADD(column-name...