NestJs 上手之路之二 【連線Mysql資料庫、專案框架調整優化】

語言: CN / TW / HK

theme: juejin highlight: arta


前言

上一篇文章是用的mongoDB資料庫,由於考慮到很多企業是用的mysql資料庫,所以我將資料庫改為mysql,並且重新調整框架和CRUD的功能,比如調整目錄結構、新增過濾器、攔截器、封裝分頁、封裝返回資料結構、表設計等等我們做專案前統一的框架調整。

目錄調整

src目錄下面分別新建common(公共程式碼)和modules(業務相關程式碼) src ├─ app.controller.spec.ts ├─ app.controller.ts ├─ app.module.ts ├─ app.service.ts ├─ main.ts ├─ common │ ├─ common // 公共dto和dto等等 │ │ ├─ dto │ │ │ ├─ base.dto.ts // 公共的類 │ │ │ ├─ pagination.dto.ts // 分頁 │ │ │ └─ result.dto.ts // 結果返回 │ │ └─ entity │ │ └─ base.entity.ts // 公共的實體 │ ├─ config // 環境配置 │ ├─ exception // 異常封裝 │ │ └─ error.code.ts // 異常的code類 │ ├─ filters │ │ └─ http-execption.filters.ts // 過濾器 │ ├─ interceptor │ │ └─ transform.interceptor.ts // 攔截器 │ ├─ pipe │ │ └─ validate.pipe.ts // 類驗證 │ └─ utils // 封裝的工具 │ ├─ convert.utils.ts │ ├─ cryptogram.util.ts │ ├─ page.util.ts │ └─ regex.util.ts └─ modules └─ users // 示例業務模組 使用者管理 ├─ dto │ ├─ create-user.dto.ts │ ├─ list-user.dto.ts │ └─ update-user.dto.ts ├─ entities │ └─ user.entity.ts ├─ users.controller.ts ├─ users.module.ts └─ users.service.ts

TypeORM 整合

連線mysql資料庫

為了與 SQL和 NoSQL 資料庫整合,Nest 提供了 @nestjs/typeorm 包。Nest 使用TypeORM是因為它是 TypeScript 中最成熟的物件關係對映器( ORM )。因為它是用 TypeScript 編寫的,所以可以很好地與 Nest 框架整合。

為了開始使用它,我們首先安裝所需的依賴項。

$ npm install --save @nestjs/typeorm typeorm mysql2

安裝過程完成後,我們可以將 TypeOrmModule 匯入AppModule 。

app.module.ts

``` import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm';

@Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nestjs', autoLoadEntities: true, // 使用這個配置自動匯入entities synchronize: true, }), ], }) export class AppModule {} `` 因為我本地是開啟熱載入了,所以對於ormconfig.json`這種方式是不支援的,如果你們那沒有開啟熱載入可以試試以下方式:

我們可以建立 ormconfig.json ,而不是將配置物件傳遞給 forRoot()

{ "type": "mysql", "host": "localhost", "port": 3306, "username": "root", "password": "root", "database": "test", "entities": ["dist/**/*.entity{.ts,.js}"], "synchronize": true }

然後,我們可以不帶任何選項地呼叫 forRoot() :

app.module.ts

``` import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm';

@Module({ imports: [TypeOrmModule.forRoot()], }) export class AppModule {} ```

靜態全域性路徑(例如 dist/**/*.entity{ .ts,.js} )不適用於 Webpack 熱過載。

增加環境配置

每個專案都有不用環境配置檔案,這樣我們切換環境修改一些配置的時候只取修改每個環境的配置檔案即可。

src下新建目錄config,在config下新建index.ts、env.development.ts、env.production.ts

env.development.ts // 開發環境配置

``` export default { // 服務基本配置 SERVICE_CONFIG: { // 埠 port: 3000, },

// 資料庫配置 DATABASE_CONFIG: { type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nestjs', autoLoadEntities: true, synchronize: true, }, }; ```

env.production.ts // 生產環境配置

``` export default { // 服務基本配置 SERVICE_CONFIG: { // 埠 port: 3000, },

// 資料庫配置 DATABASE_CONFIG: { type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'root', database: 'nestjs_prod', autoLoadEntities: true, synchronize: true, }, }; ```

index.ts ``` import development from './env.development'; import production from './env.production';

const configs = { development, production, };

const env = configs[process.env.NODE_ENV || 'development']; export { env }; ```

app.module.ts

import { Module } from '@nestjs/common'; import { APP_PIPE } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ValidationPipe } from './common/pipe/validate.pipe'; import { UsersModule } from './modules/users/users.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { env } from './common/config'; @Module({ imports: [TypeOrmModule.forRoot(env.DATABASE_CONFIG), UsersModule], controllers: [AppController], providers: [ AppService, { provide: APP_PIPE, useClass: ValidationPipe, }, ], }) export class AppModule {}

package.json

預設啟動會走開發環境配置,如果想走生產環境配置,可新增NODE_ENV=production

"start:prod": "NODE_ENV=production nest start --watch"

封裝CRUD

表設計、封裝基本欄位

一般資料庫表設計的時候,都會有幾個公共欄位(主鍵id,建立人creator,建立時間createTime,更新人updater,更新時間updateTime,刪除標誌delFlag,更新次數version)。所以我們將這幾公共欄位封裝一下,然後其他dto和entity來分別繼承這個類。

提示:資料庫表名和欄位用小寫命名,用下劃線分隔,一般咱們設計刪除資料的時候都是邏輯刪除

分別新建檔案 src=>commcon=>common=>entity=>base.entity.ts src=>commcon=>common=>dto=>base.dto.ts

base.entity.ts ``` import { Column, PrimaryGeneratedColumn, UpdateDateColumn, CreateDateColumn, VersionColumn, } from 'typeorm';

export abstract class Base { // 主鍵id @PrimaryGeneratedColumn() id: number;

// 建立時間 @CreateDateColumn({ name: 'create_time' }) createTime: Date;

@Column() // 建立人 creator: string;

// 更新時間 @UpdateDateColumn({ name: 'update_time' }) updateTime: Date;

@Column() // 更新人 updater: string;

// 邏輯刪除 @Column({ default: 0, select: false, name: 'del_flag', }) delFlag: number;

// 更新次數 @VersionColumn({ select: false, }) version: number; } ``` 特殊列

有幾種特殊的列型別可以使用: - @CreateDateColumn 是一個特殊列,自動為實體插入日期。無需設定此列,該值將自動設定。 - @UpdateDateColumn 是一個特殊列,在每次呼叫實體管理器或儲存庫的save時,自動更新實體日期。無需設定此列,該值將自動設定。 - @VersionColumn 是一個特殊列,在每次呼叫實體管理器或儲存庫的save時自動增長實體版本(增量編號)。無需設定此列,該值將自動設定。

base.dto.ts ``` import { ApiHideProperty } from '@nestjs/swagger';

export class BaseDTO { /* * 建立時間 * @example Date / readonly createTime: Date;

/* * 建立人 * @example string / creator: string;

/* * 更新時間 * @example Date / readonly updateTime: Date;

/* * 更新人 * @example string / updater: string;

/* * 是否刪除 * @example false / @ApiHideProperty() delFlag: number;

/* * 更新次數 * @example 1 / @ApiHideProperty() version: number; } `` 查詢分頁資料一般需要page第幾頁和pageSize每頁資料條數,然後結果要返回pages總頁數和total總條數還有records資料陣列,新建檔案src=>commcon=>common=>dto=>pagination.dto.ts`

pagination.dto.ts ``` import { ApiProperty } from '@nestjs/swagger'; import { IsOptional, Matches } from 'class-validator'; import { regPositiveOrEmpty } from 'src/common/utils/regex.util';

export class PaginationDTO { /* * 第幾頁 * @example 1 / @IsOptional() @Matches(regPositiveOrEmpty, { message: 'page 不可小於 0' }) @ApiProperty({ description: 'page' }) readonly page?: number;

/* * 每頁資料條數 * @example 10 / @IsOptional() @Matches(regPositiveOrEmpty, { message: 'pageSize 不可小於 0' }) @ApiProperty({ description: 'pageSize' }) readonly pageSize?: number;

/* * 總頁數 * @example 10 / pages: number;

/* * 總條數 * @example 100 / total: number;

// 資料 records: any; } `` 這裡涉及一些正則校驗,所以我們新建檔案,預設一些正則表示式src=>common=>utils=>regex.util.ts`

regex.util.ts` ``` /* * 常用正則表示式 /

// 非 0 正整數 export const regPositive = /^[1-9]\d*$/;

// 非 0 正整數 或 空 export const regPositiveOrEmpty = /\s|^[1-9]\d$/;

// 中國 11 位手機號格式 export const regMobileCN = /^1\d{10}$/g; ```

調整業務層

dao及entity繼承base

create-user.dto.ts ``` import { ApiProperty } from '@nestjs/swagger'; import { BaseDTO } from 'src/common/common/dto/base.dto';

export class CreateUserDto extends BaseDTO { @ApiProperty({ description: '使用者名稱', example: '使用者' }) userName: string;

@ApiProperty({ description: '真實姓名' }) realName: string;

@ApiProperty({ description: '密碼' }) password: string;

@ApiProperty({ description: '性別 0:男 1:女 2:保密' }) gender: number;

@ApiProperty({ description: '郵箱' }) email: string;

@ApiProperty({ description: '手機號' }) mobile: string;

@ApiProperty({ description: '部門ID' }) deptId: string;

@ApiProperty({ description: '狀態: 0啟用 1禁用' }) status: number; } ```

user.entity.ts ``` import { Base } from 'src/common/common/entity/base.entity'; import { Entity, Column } from 'typeorm';

@Entity('user') export class User extends Base { @Column({ name: 'user_name' }) userName: string;

@Column({ name: 'real_name' }) realName: string;

@Column() password: string;

@Column() gender: number;

@Column() email: string;

@Column() mobile: string;

@Column({ name: 'dept_id' }) deptId: string;

@Column({ default: 0 }) status: number; } ```

調整service

users.service.ts ``` import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { getPagination } from 'src/common/utils/page.util'; import { Not, Repository } from 'typeorm'; import { CreateUserDto } from './dto/create-user.dto'; import { ListUserDto } from './dto/list-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; import { sourceToTarget } from 'src/common/utils/convert.utils';

@Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository, ) {}

// 新增 async create(createUserDto: CreateUserDto): Promise { // 由於 createUserDto.creator = 'admin'; createUserDto.updater = 'admin'; await this.usersRepository.save(createUserDto); }

// 查詢分頁 async findAll(params): Promise { const { page = 1, pageSize = 10 } = params; const getList = this.usersRepository .createQueryBuilder('user') .where({ delFlag: 0 }) .orderBy({ 'user.update_time': 'DESC', }) .skip((page - 1) * pageSize) .take(pageSize) .getManyAndCount();

const [list, total] = await getList;
const pagination = getPagination(total, pageSize, page);
return {
  records: list,
  ...pagination,
};

}

// 根據id查詢資訊 async findOne(id: string): Promise { return await this.usersRepository.findOne(id); }

// 根據id或id和userName查詢資訊 async findByName(userName: string, id: string): Promise { const condition = { userName: userName }; if (id) { condition['id'] = Not(id); } return await this.usersRepository.findOne(condition); }

// 更新 async update(updateUserDto: UpdateUserDto): Promise { const user = sourceToTarget(updateUserDto, new UpdateUserDto()); await this.usersRepository.update(user.id, user); } } ``` 暫時寫的增刪改查的基本方法,因為是邏輯刪除,所以執行更新操作即可。如果大家還有一些基於資料庫表操作的語句想要深入瞭解,可以看看TypeORM的官方網站描述。

下面詳細說一下分頁這塊,因為新增和修改都是單獨的dto,他們之間的欄位是有區別的,那麼分頁的接收引數和返回的資料我們可以也寫一個的dto。

list-user.dto.ts ``` import { ApiProperty, PartialType } from '@nestjs/swagger'; import { PaginationDTO } from 'src/common/common/dto/pagination.dto';

export class ListUserDto extends PartialType(PaginationDTO) { @ApiProperty({ description: '使用者名稱', required: false }) userName?: string; }

``` 繼承了PaginationDTO,然後在裡面可以自定義一些查詢引數 所以這樣的話我們分頁返回的資料格式就是

{ total: 0, page: 0, pageSize: 10, pages: 0, records: [] } 在分頁裡我們還需要一個封裝法法,根據當前頁和總數和每頁的數量求共有多少頁 src=>common=>utils=>index.util.ts

index.util.ts /** * 獲取分頁資訊 * @param total * @param pageSize * @param page * @returns */ export const getPagination = ( total: number, pageSize: number, page: number, ) => { const pages = Math.ceil(total / pageSize); return { total: Number(total), page: Number(page), pageSize: Number(pageSize), pages: Number(pages), }; };

調整controller

users.controller.ts ``` import { Controller, Get, Post, Body, Param, Delete, Query, Put, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ListUserDto } from './dto/list-user.dto'; import { Result } from 'src/common/common/dto/result.dto'; import { ErrorCode } from '../../common/exception/error.code';

@Controller('users') @ApiTags('使用者管理') export class UsersController { constructor(private readonly usersService: UsersService) {}

@Post() @ApiOperation({ summary: '新增使用者資訊' }) async create(@Body() createUserDto: CreateUserDto) { const user = this.usersService.findByName(createUserDto.userName, ''); if (user) { return new Result().error( new ErrorCode().INTERNAL_SERVER_ERROR, '使用者名稱已存在', ); } await this.usersService.create(createUserDto); return new Result().ok(); }

@Get() @ApiOperation({ summary: '查詢使用者列表' }) async findAll(@Query() listUserDto: ListUserDto) { const userList = await this.usersService.findAll(listUserDto); return new Result().ok(userList); }

@Get(':id') @ApiOperation({ summary: '查詢使用者資訊' }) async findOne(@Param('id') id: string) { const user = await this.usersService.findOne(id); return new Result().ok(user); }

@Put(':id') @ApiOperation({ summary: '修改使用者資訊' }) async update(@Body() updateUserDto: UpdateUserDto) { const user = this.usersService.findByName( updateUserDto.userName, updateUserDto.id + '', ); if (user) { return new Result().error( new ErrorCode().INTERNAL_SERVER_ERROR, '使用者名稱已存在', ); } await this.usersService.update(updateUserDto); return new Result().ok(); }

@Delete(':id') @ApiOperation({ summary: '刪除使用者資訊' }) async remove(@Param('id') id: string) { const user = await this.usersService.findOne(id); if (!user) { return new Result().error( new ErrorCode().INTERNAL_SERVER_ERROR, '使用者不存在', ); } user.delFlag = 1; await this.usersService.update(user); return new Result().ok(); } } `` 在這裡我並沒有採用攔截器和過濾器去統一返回資料的格式,攔截器和過濾器我更傾向於去做一些大的東西,全域性的一些過濾和攔截,還有就是還沒有深入研究這兩個東西的使用,所以這裡我封裝了返回類。src=>common=>common=>dto=>result.dto.ts`

result.dto.ts ``` export class Result { // 狀態碼 code: number;

// 請求結果資訊 message: string;

// 資料 data: T;

ok(data = null, message = 'success') { this.code = 0; this.data = data; this.message = message; return this; }

error(code = 1, message = 'error') { this.code = code; this.message = message; return this; } } `` 封裝了ok成功和error失敗的方法,成功的時候有可能會傳data資料和message資訊,失敗的時候回傳code和message資訊,並且封裝了錯誤返回碼類src=>common=>exception=>error.code.ts`

error.code.ts ``` /* * 錯誤編碼,由5位數字組成,前2位為模組編碼,後3位為業務編碼 * 如:10001(10代表系統模組,001代表業務程式碼) / export class ErrorCode { INTERNAL_SERVER_ERROR = 500; UNAUTHORIZED = 401; FORBIDDEN = 403;

NOT_NULL = 10001; DB_RECORD_EXISTS = 10002; PARAMS_GET_ERROR = 10003; ACCOUNT_PASSWORD_ERROR = 10004; ACCOUNT_DISABLE = 10005; IDENTIFIER_NOT_NULL = 10006; CAPTCHA_ERROR = 10007; SUB_MENU_EXIST = 10008; PASSWORD_ERROR = 10009; ACCOUNT_NOT_EXIST = 10010; } ```

小結

到目前為止,mysql資料庫連線以及一些基礎的封裝以及搞定,執行程式試試吧。

如果大家需要新增一些攔截器和過濾器可以接著檢視一下章節。

攔截器

攔截器我只寫了一部分,如果大家需要可以去檢視nestjs官方網站文件

響應對映

我們已經知道, handle() 返回一個 Observable。此流包含從路由處理程式返回的值, 因此我們可以使用 map() 運算子輕鬆地對其進行改變。

響應對映功能不適用於特定於庫的響應策略(禁止直接使用 @Res() 物件)。

讓我們建立一個 TransformInterceptor, 它將打包響應並將其分配給 data 屬性。

transform.interceptor.ts

``` import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators';

export interface Response { data: T; }

@Injectable() export class TransformInterceptor implements NestInterceptor> { intercept(context: ExecutionContext, next: CallHandler): Observable> { return next.handle().pipe(map(data => ({ data }))); } } ```

main.ts async function bootstrap() { ... app.useGlobalInterceptors(new TransformInterceptor()); ... }

過濾器

異常過濾器

雖然基本(內建)異常過濾器可以為您自動處理許多情況,但有時您可能希望對異常層擁有完全控制權,例如,您可能希望基於某些動態因素新增日誌記錄或使用不同的 JSON 模式。 異常過濾器正是為此目的而設計的。 它們使您可以控制精確的控制流以及將響應的內容傳送回客戶端。

讓我們建立一個異常過濾器,它負責捕獲作為HttpException類例項的異常,併為它們設定自定義響應邏輯。為此,我們需要訪問底層平臺 Request和 Response。我們將訪問Request物件,以便提取原始 url並將其包含在日誌資訊中。我們將使用 Response.json()方法,使用 Response物件直接控制傳送的響應。

http-exception.filter.ts

``` import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; import { Request, Response } from 'express';

@Catch(HttpException) 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();

response
  .status(status)
  .json({
    statusCode: status,
    timestamp: new Date().toISOString(),
    path: request.url,
  });

} } ```

所有異常過濾器都應該實現通用的 ExceptionFilter<T> 介面。它需要你使用有效簽名提供 catch(exception: T, host: ArgumentsHost)方法。T 表示異常的型別。

@Catch() 裝飾器繫結所需的元資料到異常過濾器上。它告訴 Nest這個特定的過濾器正在尋找 HttpException 而不是其他的。在實踐中,@Catch() 可以傳遞多個引數,所以你可以通過逗號分隔來為多個型別的異常設定過濾器。

main.ts async function bootstrap() { ... app.useGlobalFilters(new HttpExceptionFilter()); ... }

總結

以上就是這次專案框架優化的內容,雖然這些只是基本的一些封裝,也總算像個樣子了,下一篇打算做下使用者的登入等功能,結合著vue3架子搭建的後臺管理前端頁面,做一些實際的業務東西。

程式碼地址:https://gitee.com/wd_591/nestjs-demo1

本文參考:https://juejin.cn/column/6992093362819956766