【NestJS系列】連線資料庫及優雅地處理響應

2023-08-29 21:00:25

前言

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.ts
import { 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.ts
import { 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.ts
import { ValidationPipe } from '@nestjs/common';

app.useGlobalPipes(new ValidationPipe()) // 全域性驗證管道

service邏輯處理,入庫操作

// user.service.ts
import { 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<User>
  ) {}

 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.ts
import { 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<Group>,
    @InjectRepository(Template)
    private templateRepository: Repository<Template>,
  ) {}
  // 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<any> {
        return next.handle().pipe(map(data => {
            return {
                statusCode: HttpStatus.OK,
                message: '請求成功',
                data
            }
        }))
    }
}

使用:

 app.useGlobalInterceptors(new HttpStatusSuccess()); // 全域性攔截器請求成功

請求: