使用NestJs、GraphQL、TypeORM搭建后端服务

2020-11-14 16:01:53 浏览数 (1)

本文介绍今年上半年使用的的一些技术,做一些个人的学习记录,温故而知新。主要包含了NestjsTypeGraphQLTypeORM相关的知识。本文示例代码以提交到github,可以在这里查看。

一、介绍

1.1、什么是NestJs?

NestJs是一个后端框架,类似于ExpressKoa。不同的是它内置并完全支持TypeScript,使用渐进式JavaScript,结合了OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。如果你使用过最新的AngularJs的话,那么你对可能会很容易上手,它最主要的特点就是,Module·Service·Controller·Provider,以及大量的使用装饰器

代码语言:txt复制
// 使用 @Module 定义一个 Module(模块)
import { PokemonResolver } from './pokemon.resolver'
import { Module } from '@nestjs/common'
import { PokemonService } from './pokemon.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { PokemonEntity } from './pokemon.entity'

@Module({
    imports: [ TypeOrmModule.forFeature([ PokemonEntity ]) ],
    providers: [ PokemonResolver, PokemonService ]
})
export class PokemonModule {}
1.2、什么是TypeGraphQL?

TypeGraphQL是基于GraphQL重写的TypeScript版本,GraphQL的全称是:Graph Query Langue 图形化查询语言,是一个可由调用端定义API返回数据结构语言。在我们过去常用的RestFul API中,我们可能在不同的业务中需要调用同一个接口,但是各自所需的数据有不同的情况下,服务端为了同时满足两个需求则提供了更多的字段,这样导致了一个两个业务请求到的数据都包含了自己不需要的字段,造成了不必要的资源浪费。GraphQL则是解决了这个问题,它可以让各个业务都可以通过一个接口拿到自己刚刚好的数据,而不用返回一个多余的字段。

代码语言:txt复制
// 使用 @ObjectType 定义一个GraphQL数据结构 
import { Field, ObjectType} from 'type-graphql'

@ObjectType()
export class CreatePokemonDto {
    @Field() readonly id?: string
    @Field() readonly name: string
    @Field() readonly type: string
    @Field() readonly pokedex: number
}

示例一:查询一个字段

示例二:查询两个字段

1.3、什么是TypeORM?

后端开发同学应该都知道ORM的全称是对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。 本质上就是将数据从一种形式转换到另外一种形式。而TypeORM则是使用TypeScript编写的JavaScript版本的ORM库。通过他我们可以定义一些Entity(实体),每个实体的数据字段,每个字段包含了数据类型,甚至是数据关系(一对多、多对多、多对一)。这些实体将映射到真实数据库中,创建真正的数据表。而数据字段和关系也就生成对应的数据库表字段以及表字段与表字段的关系。目前TypeORM已经支持mysqlpostgresmariadbsqlitecordovanativescriptoraclemssqlmongodbsqljsreact-native共计11种类型的数据库引擎的连接。

代码语言:txt复制
// 使用 @Entity 声明一个实体
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity('pokemon')
export class PokemonEntity {
    @PrimaryGeneratedColumn('uuid')
    id: string

    @Column('varchar', { length: 500, unique: true })
    name: string

    @Column('varchar', { length: 500 })
    type: string

    @Column('numeric')
    pokedex: number
}

二、项目初始化及项目初始内容解析

2.1、项目初始化

NestJs提供了CLI,可以直接使用他们的CLI工具创建项目。首先我们需要先安装CLI工具,然后使用 nest new project-name初始化项目。

代码语言:txt复制
$ npm i -g @nestjs/cli
$ nest new project-name

此处我们创建一个nest-pokemon项目,然后我们进入项目根目录使用yarn start:dev启动服务,打开http://localhost:3000 即可看到 hello word!

2.2、初始内容解析

项目内容如下所示:

代码语言:txt复制
src
├── app.controller.ts
├── app.module.ts
├── app.service.ts
└── main.ts

其中main.ts是入口文件,内容包含一个异步函数,它负责引导启动整个App。

代码语言:txt复制
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

app.module.ts是App的整个主体部分,所有的服务都从这里开始,NestJs的核心思想是万物皆Module,所以我们可以到AppModule由一个@Module装饰器进行修饰,@Module的参数是一个对象,包含三个属性:imports、controller、providers。他们分别的作用是:

  • imports:模块,用于添加App的子模块,可能是用户模块,可能是商品模块,也可能是支付模块。这里的类由@Module()装饰。
  • controller:控制器,里面用于路由控制,这里的类由@Controller()装饰。
  • providers:提供者,这里的主要功能是服务者的角色,这样的文件职责划分类似与MVC,这里的类由@Injectable()进行装饰。可以理解为依赖注入。 他们的值都为一个数组,方便添加多个模块功能。
代码语言:txt复制
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

三、添加TypeORM到项目,选择MySQL作为数据库

3.1、添加依赖,启动mysql服务

我们在src目录下创建一个modules文件夹,里面将会用来放置模块,这些模块将会被引入app.module.ts并且添加到imports字段里。再到src/modules下添加一个文件夹pokemon文件夹用于放置pokemon 模块。我们先来将TypeORM相关依赖添加到项目,依赖包括三部分,分别是NestJs支持TypeORM的依赖包@nestjs/typeormTypeORM本身typeorm,数据库支持MySQL

代码语言:txt复制
$ npm i @nestjs/typeorm --save
$ npm i typeorm --save
$ npm i mysql --save

除此之外,我们还需要开启MySQL服务,可以是本地的也是线上的。确保MySQL服务在线后,我们来改造代码。

3.2、改造app.module.ts

@nestjs/typeorm中引入NestJsTypeORM连接模块*TypeOrmModule,然后传入一个Object作为与数据库链接的 Connection,创建一个新的连接。此前有提到,在NestJs里面万物皆是Module,所以这里的TypeORM也是作为一个子Module添加到整个服务中。所以它的位置应该在imports这里

代码语言:txt复制
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      "type": "mysql",
      "host": "localhost",
      "port": 3306,
      "username": "root",
      "password": "123456789",
      "database": "nest3",
      "synchronize": true,
      "logging": false,
      "entities": []
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

其中个字段分别的意义是:

  • type:数据库类型
  • host:数据库连接host
  • port:数据库连接port
  • username:数据库管理员名称
  • password:数据库管理员密码
  • database:数据库名称
  • synchronize:指示是否在每次应用程序启动时自动创建数据库架构,不可在开发环境使用。
  • logging:日志
  • entities:要加载并用于此连接的实体。接受要加载的实体类和目录路,值为一个数组。

现在保存文件,我们将会得到一个错误,因为TypeORM生成数据库表的时候至少需要一个实体Entity文件。现在我们来src/modules/pokemon目录下创建实体文件pokemon.entity.tsTypeORM的基本方法了解:

  • Entity:实体装饰器,将一个类声明为一个实体。传入一个字符串作为参数,这个名称将用于生成表的名称,使用方式@Entity('table_name')
  • Column:列装饰器,将一个字段声明为一个数据表的一个字段,可以设置字段的数据类型,基础的校验方式,使用方式@Column('varchar', { length: 500, unique: true })
  • PrimaryGeneratedColumn:主键装饰器,将一个字段声明为主键,对应数据库表字段的主键。

值得注意的是:@Entity只能装饰类,@Column只能装饰类属性

代码语言:txt复制
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity('pokemon')
export class PokemonEntity {
    @PrimaryGeneratedColumn('uuid')
    id: string

    @Column('varchar', { length: 500, unique: true })
    name: string

    @Column('varchar', { length: 500 })
    type: string

    @Column('numeric')
    pokedex: number
}

这时候我们引入这个实体到app.module.ts中,代码为如下所示:

代码语言:txt复制
import { Module } from '@nestjs/common';
import { AppService } from './app.service';
import { AppController } from './app.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PokemonEntity } from "./modules/pokemon/pokemon.entity";


@Module({
  imports: [
    TypeOrmModule.forRoot({
      "type": "mysql",
      "host": "localhost",
      "port": 3306,
      "username": "root",
      "password": "123456789",
      "database": "nest3",
      "synchronize": true,
      "logging": false,
      "entities": [PokemonEntity]
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

现在保存代码,我们可以看到错误已经消失了,我们进入mysql服务,查看数据多了一个名字叫nest3的数据库,选择它,我们可以查看到已经创建了pokemon表,使用desc pokemon;查看表详情:

pokemonpokemon

到目前为止,我们的已经成功把TypeORM添加到了项目中,下一步添加GraphQL。

四、添加TypeGraphQL到项目中

4.1、安装依赖与功能说明

同上,NestJs官方也支持了GraphQL,对于TypeGraphQL我们有两种选择方式,一是安装type-graphql,二是直接使用@nestjs/graphql,这里我们直接使用@nestjs/graphql。 我们先把需要的依赖安装下:

代码语言:txt复制
$ npm i @nestjs/graphql --save

因为Graphql需要依赖具体的事务,所以我们在src/modules/pokemon目录下创建三个文件,分别是:pokemon.module.tspokemon.service.tspokemon.resolver.tspokemon.module.ts用于声明pokemon模块,pokemon.service.ts则具体与数据库做交互,pokemon.resolver.ts则用于创建GraphQL相关的QueryMutation我们将在这个示例中首先两个功能,创建一个pokemon,查询全部的pokemon

4.2、GraphQL基本方法说明与Schema声明

首先GraphQL常用的几个装饰器方法分别是:

  • ObjectType:声明一个Schema(数据结构),对一个类进行装饰,用于声明这个Object的各个字段以及他们的类型,使用方式:@ObjectType
  • Field:声明一个属性,这个属性属于ObjectType在进行API查询的时候将会用于解释一个字段,它对类的一个属性进行装饰,使用方式:@Field
  • InputType:声明一个输入类型的Schema,当进行Mutation变异查询(提交数据)的时候,提交的数据格式必须要按照此结构提交,使用方式:InputType。同@ObjectType对一个类进行装饰。

现在我们声明一个Pokemon ObjectType,我们在src/modules/pokemon创建一个文件夹dto(Data Transfer Object),然后在src/modules/pokemon/dto目录下创建create-pokemon.dto.ts,内容为以下:

代码语言:txt复制
import { Field, ObjectType} from 'type-graphql'

@ObjectType()
export class CreatePokemonDto {
    @Field() readonly id?: string
    @Field() readonly name: string
    @Field() readonly type: string
    @Field() readonly pokedex: number
}

这个文件的内容做了以下行为:

  • 声明一个叫做CreatePokemonDto的类,并且使用了@ObjectType()进行修饰,所以它可以被当作一个TypeGraphQLSchema
  • 这个类声明了四个只读属性的字段,并且定义了输入的数据类型。
4.3、声明pokemon模块,并引入到App中

到目前为止,我们以及创建好了TypeORMentity实体,TypeGraphQLObjectType,现在我们先声明PokemonModule

代码语言:txt复制
import { Module } from '@nestjs/common'

@Module({
    imports: [],
    providers: []
})
export class PokemonModule {}

现在,我们把它们融合到pokemon.module.ts中,但在此之前,我们需要声明PokemonModuleProviders,一是在pokemon.service.ts中提供与数据库交互的方法。这两个方法即是我们之前提到的创建以及查询一个列表代码如下:

代码语言:txt复制
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { PokemonEntity } from './pokemon.entity'
import { Repository } from 'typeorm'

@Injectable()
export class PokemonService {
    constructor (
        @InjectRepository(PokemonEntity) 
        private readonly PokemonRepository: Repository<PokemonEntity>
    ) {}
    async getPokemons () {}
    async createPokemon (data: CreatePokemonDto): Promise<PokemonEntity> {}
}

第二是在当前目录下创建pokemon.resolver.ts,这个文件的内容将用于提供GraphQL的方法,代码如下:

代码语言:txt复制
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'
import { PokemonEntity } from './pokemon.entity'
import { CreatePokemonDto } from './dto/create-pokemon.dto'
import { PokemonService } from './pokemon.service'
import { inputPokemon } from './input/pokemon.input'

@Resolver(() => PokemonEntity)
export class PokemonResolver {
    constructor (private readonly pokemonService: PokemonService) {}

    @Query(() => [ CreatePokemonDto ])
    async pokemon () {
        return this.pokemonService.getPokemons()
    }

    @Mutation(() => CreatePokemonDto)
    async createPokemon (@Args('data') data: inputPokemon) {
        return this.pokemonService.createPokemon(data)
    }
}

其中:inputPokemon是一个TypeGraphQL的输入类型的Schema,文件将放在src/mmodules/pokemon/input目录下,代码如下:

代码语言:txt复制
import { Field, InputType } from 'type-graphql'

@InputType()
export class inputPokemon {
    @Field() readonly name: string
    @Field() readonly type: string
    @Field() readonly pokedex: number
}

这个文件声明了inputPokemon的输入类型,并且规定了属性以及属性的数据类型。

现在开始改造pokemon.module.ts,代码如下:

代码语言:txt复制
import { PokemonResolver } from './pokemon.resolver'
import { Module } from '@nestjs/common'
import { PokemonService } from './pokemon.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { PokemonEntity } from './pokemon.entity'

@Module({
    imports: [ TypeOrmModule.forFeature([ PokemonEntity ]) ],
    providers: [ PokemonResolver, PokemonService ]
})
export class PokemonModule {}

这里的TypeOrmModule.forFeature()则是给当前模块提供功能的子模块,表示当前模块会使用到TypeORMproviders内包含两个元素,分别是提供GraphQL功能的PokemonResolver,提供与数据交互的PokemonService。我们现在把PokemonService的功能完善如下:

代码语言:txt复制
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { PokemonEntity } from './pokemon.entity'
import { Repository } from 'typeorm'
import { CreatePokemonDto } from './dto/create-pokemon.dto'

@Injectable()
export class PokemonService {
    constructor (
        @InjectRepository(PokemonEntity) 
        private readonly PokemonRepository: Repository<PokemonEntity>
    ) {}
    async createPokemon (data: CreatePokemonDto): Promise<PokemonEntity> {
        let pokemon = new PokemonEntity()
        pokemon.name = data.name
        pokemon.pokedex = data.pokedex
        pokemon.type = data.type
        await this.PokemonRepository.save(pokemon)
        return pokemon
    }

    async getPokemons () {
        return await this.PokemonRepository.find()
    }
}

最后,我们将PokemonModule引入到app.module.ts中,并且在里面注入TypeGraphQL的功能模块,代码如下:

代码语言:txt复制
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { PokemonModule } from './modules/pokemon/pokemon.module'
import { PokemonEntity } from "./modules/pokemon/pokemon.entity";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      "type": "mysql",
      "host": "localhost",
      "port": 3306,
      "username": "root",
      "password": "123456789",
      "database": "nest3",
      "synchronize": true,
      "logging": false,
      "entities": [PokemonEntity]
    }),
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gpl'
    }),
    PokemonModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

保存以上文件,我们打开 http://localhost:3000/graphql ,即可看到GraphQL的调试界面。

结语

其实总体来说,整个NestJs应用的开发体验还是蛮好的,相关的生态也发展的比较成熟,本项目仅是对相关技术的一个整体尝试,实际在开发过程中遇到的还有很多别的问题,比如GraphQLN 1查询问题,前后端分离应用的登陆认证问题等等...TypeORM也并不是很完美的技术,当应用有比较复杂的查询关系的时候,效率会低下,相关代替产品有SequelizePrisma等等技术都可以代替掉。个人可以根据实际需求,对比优缺点进行选择。

学而时习之,不亦说乎。温故而知新,可以为师矣。努力努力~

0 人点赞