Nest.js 快速入门:实现对 Mysql 单表的 CRUD

5,420 阅读8分钟

Nest.js 是一个基于 Node.js 的后端开发框架,它实现了 MVC 模式,也就是 Model、View、Controller 的分层,也支持了 IOC,也就是可以自动注入依赖,比 Express 这类处理请求响应的库高了一个层次。而且也很容易集成 GraphQL、WebSocket 等功能,适合用来做大规模企业级开发。

Nest.js 在国内外用的都挺多的,今天我们就来入门下吧:做一个笔记管理功能,实现对 mysql 单表的增删改查并提供 Restful 的接口。

完整代码: github.com/QuarkGluonP…

Nest.js + Typeorm 基础

mysql 数据库和 Typeorm

首先从离前端比较远的数据库讲起。

在 mysql 的官网下载 mysql,安装并启动服务。

这时候就可以用命令行来写 sql 操作数据库了。

但是命令行操作不够方便,所以我们还要下载一个有界面的 mysql 客户端,我这里用的是 navicat。

它可以可视化的创建数据库、表等,可以在编辑器里写 sql 然后执行。比如图中我创建了 hello 的数据库和一堆表。

Node.js 代码里同样可以连接上数据库服务,然后远程执行 sql 来对数据库表做增删改查。

但直接执行 sql 比较繁琐,能不能我只操作对象,对象属性变了就自动去执行 sql 来同步数据库呢?就像 vue 的数据变了自动同步视图一样。

数据库和对象关系的映射就叫做 ORM(Object Relational Mapping),也就是把表映射成对象,把表与表之间的关联映射成对象之间的关系。之后对对象的操作会通过 sql 同步到数据库。

Typeorm 就是一个实现 orm 的框架,可以通过装饰器来描述映射关系,比如 @Entity(实体)、@Column(列)、@PrimaryGeneratedColumn(主键 ID 自动生成)

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class Note{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    content: string;
}

通过装饰器声明了关系,那么在建立了数据库连接之后,我们只需要操作对象,Typeorm 就会自动去执行 sql 来把变动同步到数据库。

这样,我们对数据库的表的操作和增删改查就实现了。

数据库部分搞定之后,我们再往前看一下处理请求的部分。

http 请求和 Nest.js

处理请求的后端框架我们使用 Nest.js,它提供了 Controller、Service 等划分,这是对 MVC 模式的实现。

Controller 里面负责处理请求,把处理过的参数传递给 service。

Service 负责业务逻辑的实现,基于 Typeorm 的增删改查功能来实现各种上层业务逻辑。

除此以外,Nest.js 还划分了 Module,这个 Module 是逻辑上的模块,和我们常说的文件对应的模块不同,它包含了 Controller、Service 等,是对这些资源的逻辑划分。

Module 和 Module 之间还可以有依赖关系,也就有 imports 和 exports。

所以,模块的声明就是这样的:

import { Module } from '@nestjs/common';

@Module({
  imports: [AaaModule],
  controllers: [BbbController],
  providers: [BbbService],
  exports: [BbbService]
})
export class BbbModule {}

这里通过 @Module 的装饰器来声明了 Bbb 的模块,它依赖了 Aaa 模块,也就是在 imports 引入的 AaaModule。controllers 是控制器,包含 BbbController,providers 是提供商,有 service、factory 等类型,这里包含 BbbService,同时,还导出了 BbbService 可以被其他模块引入。

Controller 的声明也是通过装饰器:

@Controller()
export class BbbController {
}

Service 的声明也是用装饰器,只不过不叫 Service,而叫 Injectable。

@Injectable()
export class BbbService {
}

至于为什么叫 Injectable,就涉及到了 IOC 的概念了。

IOC(Inverse Of Control)是控制反转的意思,就是只需要声明你的依赖,不需要创建依赖的对象,容器会注入给你。

因为所有的对象都是由容器管理的,那么自然就可以在创建对象的时候注入它需要的依赖,这就是 IOC 的原理。

Service 是可以被作为依赖注入到其他类的实例中去的,所以用 Injectable 装饰器。

所有的 Module 会有一个根 Module 作为入口,启动 IOC 容器就是从这个模块开始的:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import "reflect-metadata";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

上面就是典型的 Nest.js 启动代码,从 AppModule 这个根 Module 开始创建 IOC 容器,处理从 3000 端口发过来的请求。

reflect-metadata 模块是用于解析类的装饰器的,因为要给某个类的实例注入依赖就得能解析出它通过装饰器声明了哪些依赖,然后注入给它。所以要实现 IOC 需要依赖这个包。

这就是 Nest.js 大概的设计了:IOC + MVC,通过 IOC 容器来管理对象的依赖关系,通过 Controller、Service、Module 来做职责上的划分。

Nest.js 结合 Typeorm

Typeorm 是做把对象的操作通过 sql 同步为对数据库操作的 orm 的,而 Nest.js 是做 Web 后端应用的 MVC 分层以及通过 IOC 管理对象的创建和依赖的。这俩很自然的可以结合,结合的方式就是 @nestjs/typeorm 包。

@nestjs/typeorm 包提供了 TypeOrmModule 这个 Module,它有两个静态方法 forRoot、forFeature。

forRoot 用于创建数据库连接,传入一些配置参数,在入口 Module 引入。

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '你的密码',
      database: '数据库名',
      synchronize: true
    }),
    NotesModule
  ]
})
export class AppModule {}

forFeature 用于创建不同实体类对应的 Repository,在用到该实体的 Module 里引入。

@Module({
  imports: [TypeOrmModule.forFeature([Aaa])],
  controllers: [AaaController],
  providers: [AaaService],
  exports: [AaaService]
})
export class AaaModule {}

我们知道了 Typeorm 和 Nest.js 都是做什么的和怎么用,简单小结一下:

Typeorm 是 ORM 框架,用于把对象的操作同步为对数据库的操作,会自动执行 sql 语句。

Nest.js 是 MVC 框架,用于 Web 后端应用的逻辑分层,还提供了 Module 用来进一步划分 Controller 和 Service。此外,Nest.js 提供了 IOC 容器,统一管理对象的创建和依赖关系,根据声明来自动注入依赖。

两者的结合就是通过 @nestjs/typeorm 的包,它有两个静态方法用于生成 Module。

说了这么多,大家可能还理解的不是很清楚,那么我们就来做下笔记管理的实战案例吧。

实战案例

Nest.js 样板代码比较多,自己写还是比较费事的,@nestjs/cli 的命令行工具对这些做了自动化。

首先要搭项目的骨架,用

nest new project-name

然后生成某个 Module 的代码

nest g resource xxx

生成的代码就是带有 Controller、Service、Module 的,并且也有了 CRUD 的样板代码。

我们重点来看下 Controller 的代码:

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { XxxService } from './xxx.service';
import { CreateXxxDto } from './dto/create-xxx.dto';
import { UpdateXxxDto } from './dto/update-xxx.dto';

@Controller('xxx')
export class XxxController {
  constructor(private readonly xxxService: XxxService) {}

  @Post()
  create(@Body() createXxxDto: CreateXxxDto) {
    return this.xxxService.create(createXxxDto);
  }

  @Get()
  findAll() {
    return this.xxxService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.xxxService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateXxxDto: UpdateXxxDto) {
    return this.xxxService.update(+id, updateXxxDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.xxxService.remove(+id);
  }
}

@Controller 的参数可以声明 URL 路径,@Get、@Post、@Patch、@Delete 也可以通过参数声明 URL 路径,最终会把两个拼起来。比如 /xxx/:id 的 get 方法。

@Get、@Post、@Patch、@Delete 分别对应不同的请求方式。

@Param 是取路径中的参数,@Query 是取查询字符串的参数。

@Body 是把请求参数设置到对象的属性上,被用来传递数据的对象叫做 dto(data transfer object)。

再就是返回的对象会被序列化成 JSON,不需要手动序列化。

然后再看下 Service:

import { Injectable } from '@nestjs/common';
import { CreateXxxDto } from './dto/create-xxx.dto';
import { UpdateXxxDto } from './dto/update-xxx.dto';

@Injectable()
export class XxxService {
  create(createXxxDto: CreateXxxDto) {
    return 'This action adds a new xxx';
  }

  findAll() {
    return `This action returns all xxx`;
  }

  findOne(id: number) {
    return `This action returns a #${id} xxx`;
  }

  update(id: number, updateXxxDto: UpdateXxxDto) {
    return `This action updates a #${id} xxx`;
  }

  remove(id: number) {
    return `This action removes a #${id} xxx`;
  }
}

这些 service 的方法都没有具体实现。

我们引入 Typeorm 来做数据库的 CRUD。

在根模块引入用于数据库连接的 Module

在刚创建的模块引入实体对应的 Module:

创建笔记实体,用 @Entity 标识。并且用 @Column、@PrimaryGeneratedColumn 来标识列和主键。

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class Note{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    content: string;

    @Column()
    createTime: Date;

    @Column()
    updateTime: Date;

    @Column()
    isDelete: boolean;
}

之后在 service 里注入实体对应的操作类 Repository,就可以实现对笔记的增删改查了。

用到的 dto 就是参数对应的对象,他们是实体的一部分属性的集合。比如 update dto:

export class UpdateNoteDto {
    title: string;

    content: string;

    createTime: Date;

    updateTime: Date;

    isDelete: boolean;
}

这样,就实现了对笔记的增删改查。

我们用 postman 来测试下效果:

运行 npm start 把项目跑起来

可以看到 4 个接口的路由映射都成功了。

数据库一开始有两条记录:

通过查询接口能正确的查出来:

然后测试下修改接口:

数据库中确实被修改了:

经过测试,对笔记单表的 CRUD 的功能正常。

我们完成了第一个 Nest.js 的后端应用!

完整代码上传了 github: github.com/QuarkGluonP…

总结

Typeorm 是一个 ORM 框架,通过映射表和对象的对应关系,就可以把对对象的操作转换为对数据库的操作,自动执行 sql 语句。

Nest.js 是一个 MVC 框架,提供了 Module、Controller、Service 的逻辑划分,也实现了 IOC 模式,集中管理对象和自动注入依赖。

Typeorm 和 Nest.js 的结合使用 @nestjs/typeorm 的包,它提供了一个 TypeormModule 的模块,有 forRoot 和 forFeature 两个静态方法。forRoot 方法用于生成连接数据库的 Module,forFeature 用于生成实体对应的 Repository 的 Module。

Nest.js 有很多样板代码,可以用 @nestjs/cli 的命令行工具生成,包括整体的和每个 Module 的。

总之,理解了 IOC,理解了 Module、Controller、Service 的划分,就算是初步掌握了 Nest.js,结合 Typeorm 的 ORM 框架可以轻松的做数据库表的 CRUD。

Nest.js 是比较强大比较流行的一个后端框架,还是有必要好好学一下的。这篇文章我们入了下门,后面会继续深入。