type-graphql初体验 – 增删改查

最近在用graphql来重写原来的数据查询方式,难免要重新学习如何编写增删改查,网上对于查的文档比较多,但是对于增删改的记录都比较少,本文旨在记录node server端的体验记录。

阅读本文之前,请先确认已经接入了graphql,可先参照例子

因使用的框架是Nest.js,不免要看文档是如何使用的,文档中提供了两种编写方式,一种是直接编写SDL,一种是通过代码的方式来生成SDL,所以本文采用第二种方案,type-graphql

我们先简单定义一个User实体。

import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { Field, ID, ObjectType } from 'type-graphql';

@ObjectType()
@Entity()
@Unique(['username'])
export class User {
  @Field(type => ID)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column()
  username: string;

  @Field()
  @Column()
  password: string;
}

可以看出User实体就两个字段,一个username,一个password,剩下的那个是自增长的id。

从行首引入的包可以看出,我是typeorm和graphql的字段定义是放在一起的,这样可以节省代码,也不需要两边维护。

这样一个简单的实体就定义好了。

接下来是如何使用graphql和typeorm结合在一起。

我们先来确认一下接入graphql需要新建的定义,resolver,这是原来使用typeorm所没有的概念。

resolver的使用意义就是建立一个graphql的数据映射。

我目前使用的数据链路是这样的:

graphql client => resolver => service => typeorm

这么一个链路还是比较直观的,可以很简单的分析出来,新建一个resolver,需要注入service依赖。

我们先简单写一个

import { NotFoundException } from '@nestjs/common';
import { Query, Resolver } from '@nestjs/graphql';
import { User } from './user.entity';
import { UserService } from './user.service';

@Resolver(of => User) // 对该类进行Resolver定义,类似REST框架中的控制器
export class UserResolver {
  constructor(
    private readonly userService: UserService, // 注入UserService
  ) {}

  @Query(returns => User) // 添加Query装饰器,把方法标记为graphql查询
  async user(@Args('id') id: number): Promise<User> { // 定义入参出参
    const user = await this.userService.findOne(id); // 使用service进行查询
    if (!user) {
      throw new NotFoundException(id);
    }
    return user; // 返回user
  }

  @Query(returns => User) // 添加Query装饰器,把方法标记为graphql查询
  async userByUsername(@Args('username') username: string): Promise<User> { // 定义入参出参
    const user = await this.userService.findOneByUsername(username); // 使用service进行查询
    if (!user) {
      throw new NotFoundException(username);
    }
    return user; // 返回user
  }
}

上面的代码还是比较清晰易懂的,跟用控制器时候的开发体验比较一致,获取入参,通过入参查询数据,之后返回数据。

那么经过上述定义后,type-graphql会自动生成schema。(感叹号是必填的意思)

type Query {
  user(id: Float!): User!
  userByUsername(username: String!): User!
}

type User {
  id: ID!
  username: String!
  password: String!
}

调试

同时可以打开http://localhost:3000/graphql 进行接口调用。

通过id查询

query {
  user(id: 1) {
    username,
    password
  }
}

返回

{
  "data": {
    "user": {
      "username": "root",
      "password": "root"
    }
  }
}

使用用户名查询

query {
  userByUsername(username: "root") {
    username,
    password
  }
}

返回

{
  "data": {
    "userByUsername": {
      "username": "root",
      "password": "root"
    }
  }
}

直接上代码

@Mutation(returns => User) // 定义为graphql改动
async create(@Args('payload') payload: UserCreateInput): Promise<User> {// 创建入参
  return await this.userService.create(payload);
}

依然比较简单,这里把Query改为Mutation,就是把查询操作改为更改操作。

内容体中的语句依然是符合控制器时期的习惯的。

唯一需要注意的是入参,这边定义了一个UserCreateInput去定义入参,并用class-validator去验证入参,同时用Partial<User>去表示是User实体的子集

import { Length, MaxLength } from 'class-validator';
import { Field, InputType } from 'type-graphql';
import { User } from './user.entity';

@InputType()
export class UserCreateInput implements Partial<User> {
  @Field()
  @MaxLength(30)
  username: string;

  @Field()
  @Length(8, 255)
  password: string;
}

调试

mutation {
  create(payload: { username: "root2", password: "root2" }) {
        id,
    username,
    password
  }
}

返回

{
  "data": {
    "create": {
      "id": "2",
      "username": "root2",
      "password": "root2"
    }
  }
}

可以看到返回的是新创建的user的数据。

直接上代码

@Mutation(returns => User)
async update(@Args('id') id: number, @Args('payload') payload: UserUpdateInput): Promise<User> { // 注意这边新定义了UserUpdateInput,用于定义可更新的值
  return await this.userService.update(id, payload);
}

可以看出来其实是类似增的一个写法,也是Mutation,所以这次就不过多讲述了。

不过有些人可能对返回一个User感到不习惯,因为其实并不需要更新的数据,可能只是一个简单的post请求。

那么就可以针对resolve方法进行更改

@Mutation(returns => Boolean) // 更改返回为boolean值
async update(@Args('id') id: number, @Args('payload') payload: UserUpdateInput): Promise<User> {
  await this.userService.update(id, payload); // 更新
    return true; // 返回true,表示更新完成,假如上述更新出错,那么将会抛出错误,并且不会走到这一行
}

@Mutation(returns => Boolean)
async remove(@Args('id') id: number): Promise<boolean> {
  await this.userService.remove(id);
    return true;
}

删除就可以直接用到之前提到直接返回布尔值的方式。

mutation {
  remove(id: 2)
}

返回

{
  "data": {
    "remove": true
  }
}

残留问题

  • 能不能通过重载,实现自动识别入参,达到数字时查询id,字符串时查询用户名,对象时通过筛选条件查询用户。
  • 我为了约束入参,定义了UserCreateInput和UserUpdateInput,能不能通过User实体的配置解决,否则依然是维护多套代码。这可能是ts的使用范畴,需要学习。
  • 我目前就一个User实体,定义了增删改的mutation,但是名字过于通用,无法与后续代码区分。不知道实际项目会怎么处理这个问题。需要查看一些开源项目来学习。