前言

进行spring开发时,基于注解的AOPDI的编程风格以及ORM框架的良好支持使得编程层次分明,十分顺滑,但Node中,常规的使用koaexpress开发时总要一堆路由函数,且控制层与业务层无法分开,体验不是那么好,而nest提供了基于装饰器的依赖注入以及面向切面的编程风格,typeorm实现了node上的orm框架,并且支持MysqlPostgresSQL Server等等众多数据库,两者结合,可以实现基于装饰器的类似spring的开发体验,并且nest兼容express中间件,因此又可以发挥现有丰富中间件的强大功能,这里就两者做一个结合。

创建Nest工程

当然,第一步首先是要创建一个nest工程,nesttypeorem主要是基于ts的,因此首先需安装ts模块:

yarn global add typescript

然后安装nest脚手架:

yarn global add @nestjs/cli

利用脚手架创建nest工程:

nest new NestWithOrm

开始会让选择使用npm还是yarn,根据个人喜好选择即可,完成后,会生成一个nest-with-orm工程(大写自动转成了中划线),将其重命名成NestWithOrm,然后进入NestWithOrm文件夹,执行运行命令即可:

yarn run start

打开浏览器,访问3000端口,即可看到HelloWord输出。观察工程结构,所有有效代码均放在了src目录中,其中main.ts是入口文件,我们先删除除app.service.ts以及app.controller.ts文件,我们要自己组织目录结构并编写控制器等。

组织Nest结构

一般mvc式风格分service层、entity层、controller层等,我们也以类似的结构组织工程,在src下新建entitycontrollerserviceprovider几个文件夹,src的目录结构将如下所示:

src
|----entity   # 放置实体类
|----controller  # 放置控制层代码
|----service  # 放置服务层代码
|----provider  # 放置其他可依赖注入的提供者,如数据库连接池
|----app.module.ts  # 提供可注入对象以及控制器注册
`----main.ts  # 入口函数

集成TypeOrm

安装依赖

# typeorm核心模块
yarn add typeorm --save
# 为typeorm的装饰器提供元数据添加支持
yarn add reflect-metadata --save
# 提供类型定义支持
yarn add install @types/node --save
# 提供数据库驱动,使用哪种数据库,则相应安装哪种数据库的驱动
yarn add mysql2 --save

定义实体类

src/entity下新建一个User.ts,编写一个实体类,如下:

// User.ts
import {Entity, Column, PrimaryGeneratedColumn} from 'typeorm'

@Entity(“user”)
export class User {
    @PrimaryGeneratedColumn() id: number //主键,自增
    @Column() name: string
    @Column() password: string
}

定义mysql连接提供者

src/provider中新建文件MysqlProvider.ts,用于通过依赖注入的方式提供mysql连接,如下:

//MysqlProvider.ts
import path = require('path')
import { createConnection } from 'typeorm'

export = {
    provide: 'MysqlConn', //提供者的名称,通过依赖注入获取对象时即根据此名称获取
    useFactory: async () => { //提供的内容
        let conn = await createConnection({
            type: 'mysql',
            host: 'localhost',
            port: 3306,
            username: "root",
            password: "root123",
            database: 'test',
            entities: [path.join(__dirname, '../entity/*.js')],
            synchronize: true
        })
        return conn //返回一个连接对象
    }
}

这里有两点要注意,一是一般提供者,是一个加入@injectable()装饰器的普通类,mysql的连接过程放在构造函数中,但数据库链接过程是异步的,而构造函数中又不允许使用await关键词,因此可能出现类已经被注入了但mysql连接还未就绪的情况,这时若有请求,就会出现undefined错误。因此,我们通过上面的形式定义提供者可以保证类被注入时连接一定是就绪的。

第二点是可以看到创建连接时有一个entities字段,这个字段指定了要被装载的实体类对象,每个实体类对象会有一个到数据库中表的映射,这里传入了一个路径,代表扫描src/entity目录下的所有js文件,都当作实体类装载。这里写*.js而不是*.ts是因为ts最终是编译为js运行的,运行时实际上运行的是编译后的js文件。当然,也可以手动指定要装载的对象,如下所示:

entities: [User]

定义Service

src/service目录下新建UserService.ts文件,定义业务层内容,如下:

import { Injectable, Inject } from "@nestjs/common"
import { Repository, Connection, FindConditions, FindOneOptions, SaveOptions } from "typeorm"
import { User } from "../entity/User.js"

//加入@Injectable()装饰器使其可以被注入
@Injectable() export class UserService {
    private userRep: Repository<User>
    /**
    通过构造函数注入mysql连接,@Inject装饰器中传入的即为提供者的名称,也即提供者provide字段对应的内容,注入的对象即为提供者useFactory对应方法返回的内容
    **/
    constructor(@Inject('MysqlConn') private readonly mysqlConn: Connection) {
        this.userRep = this.mysqlConn.getRepository(User)
    }

    async findOne(conditions?: FindConditions<User>, options?: FindOneOptions<User>): Promise<User> {
        return this.userRep.findOne(conditions, options)
    }

    async save(user: User, options?: SaveOptions): Promise<User> {
        return this.userRep.save(user, options)
    }
} 

定义controller

src/controller目录下新建UserController.ts文件,定义控制层内容,如下:

import { Controller, Get, Param } from '@nestjs/common'
import { UserService } from '../service/UserService'

@Controller("user")
export class AppController {
    //通过构造函数注入service
    constructor(private readonly userService: UserService) {
    }
    
    //定义一个路由
    @Get("/")
    async getUser(): Promise<JSON> {
        let user = await this.userRep.save({name: 'user1', password: 'pass1'})
        let user = await this.userService.findOne({id: 1})
        return JSON.parse(JSON.stringify(user));
    }
}

注册

我们以上所有用到的要注入的类以及控制器,都要在app.module.ts中进行注册,编辑app.module.ts文件,如下:

import { Module } from '@nestjs/common'
import { UserController } from './controller/UserController'
import { UserService } from './service/UserService'
import MysqlProvider = require('./provider/MysqlProvider')

@Module({
  imports: [],
  controllers: [UserController], //注册所有控制器
  providers: [MysqlProvider, UserService] //注册所有@injectable()修饰的类以及自定义的提供者
})
export class AppModule {}

引入reflect-metadata

最后,typeorm的装饰器需要reflect-metadata模块的支持,我们需要在入口函数main.ts中显式引入一下该模块。且要放在引入的第一行,src/main.ts入口函数将如下所示:

import "reflect-metadata" //显示引入一下
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

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

最后

通过上述步骤,nesttypeorm已经集成完毕,启动工程:

yarn run start

然后浏览器访问localhost:3000/user,将会看到以下输出:

{
    "id": 1,
    "name": "user1",
    "password": "pass1"
}