阅读 238

GraphQL + TypeORM 黄金搭档

简介

本文是《全栈开发 GraphQL + Flutter 最佳实践,文章系列》中的一员。主要介绍 GraphQL 是如何以 MySQL 作为数据源,来提供增删改查操作“接口”的:

  • 查询博文
  • 新增博文
  • 修改博文
  • 删除博文

项目依赖

第 0 步

打开文件 src/index.ts,找到 main 方法。清理测试代码,将其改为:

async function main() {

  try {

    const schema = await buildSchema({
      resolvers: [PostResolver],
      dateScalarMode: 'timestamp'
    });

    const connection = await createConnection({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'tinylearn',
      password: 'my_password',
      database: 'tinylearn',
      synchronize: true,
      entities: [PostEntity],
    });

    const server = new ApolloServer({
      schema,
      playground: true
    });

    const { url } = await server.listen(4444);

    console.log(`GraphQL Playground available at ${url}`);
    
  } catch (error) {
    console.error(error);
  }
}
复制代码

运行:

$ npm start
复制代码

Playground 测试:

依赖注入

当前的 posts 方法是这样的。

@Resolver(Post)
class PostResolver {

  @Query(returns => [Post])
  async posts(): Promise<Post[]> {
    return [...];
  }
}
复制代码

这个方法无法获取数据库的 Connection,也就无法操作数据库。

要解决这个问题,我们可以定义一个 AppContext

...

type AppContext = {
  connection: Connection,
};

async function main() {...}

...
复制代码

注意需要引入import { ..., Connection } from 'typeorm';

然后在创建 server 时,传入一个 context 参数:

async function main() {

  try {

    ...

+    const context: AppContext = {
+      connection
+    };

    const server = new ApolloServer({
+      context,
      schema,
      playground: true
    });

    ...
    
  } catch (error) { ... }
}
复制代码

这样我们的 posts 方法就可以访问 AppContext 了。

@Resolver(Post)
class PostResolver {

  @Query(returns => [Post])
  async posts(
    @Ctx() context: AppContext,     // 可以从 AppContext 中获取依赖
  ): Promise<Post[]> {
    const postRepository = context.connection.getRepository(PostEntity);
    ...
  }
}  
复制代码

查询博文

找到以下代码:

@Resolver(Post)
class PostResolver {

  @Query(returns => [Post])
  async posts(): Promise<Post[]> {
    return [...]
  }
}
复制代码

posts 方法改为:


  @Query(returns => [Post])
  async posts(
    @Ctx() context: AppContext,
  ): Promise<Post[]> {
    const postRepository = context.connection.getRepository(PostEntity);
    return await postRepository.find();
  }
复制代码

注意需引入 import { ..., Ctx } from "type-graphql";

它会返回数据库里面所有的博文。

重启服务 ctrl+C, $ npm start

在 Playground 测试:

数据为空,因为数据库里面真的没有数据😄。

接下来我们就尝试新增一篇博文。

新增博文

posts 方法下面,加入 createPost

  ...
  
  async posts(
    ...
  ): Promise<Post[]> { ... }
  
  @Mutation(returns => Post)
  async createPost(
    @Ctx() context: AppContext,
    @Arg('content') content: string,
  ): Promise<Post> {
    const postRepository = context.connection.getRepository(PostEntity); 
    const post = await postRepository.save({ content });
    return post;
  }
  
  ...
复制代码

它会引入一个新增博文的 GraphQL “接口”:

GraphQL 里面 QueryMutation 都是入口,Query 主要是负责Mutation 主要是负责

而创建博文是一个操作,所以用 Mutation

重启服务 ctrl+C, $ npm start

刷新浏览器,在 Playground 测试:

检验结果:

(我们还可以通过 MySQL Workbench 来检验结果,可参考 如何使用 MySQL Workbench 图形化工具来操作数据库?

查询 posts 已经可以返回新增的博文了。

修改博文

  @Mutation(returns => Post)
  async updatePost(
    @Ctx() context: AppContext,
    @Arg('postId') postId: string,
    @Arg('content') content: string,
  ): Promise<Post> {
    const postRepository = context.connection.getRepository(PostEntity); 
    await postRepository.update(postId, { content });
    return await postRepository.findOneOrFail(postId);
  }
复制代码

重启服务 ctrl+C, $ npm start

刷新浏览器,在 Playground 测试:

这里传入的 postId 是查询博文时返回的。

检验结果:

博文内容已经被更新为: "graphql"。

删除博文

  @Mutation(returns => String)
  async deletePost(
    @Ctx() context: AppContext,
    @Arg('postId') postId: string
  ): Promise<string> {
    const postRepository = context.connection.getRepository(PostEntity); 
    await postRepository.delete(postId);
    return postId;
  }
复制代码

重启服务 ctrl+C, $ npm start

刷新浏览器,在 Playground 测试:

检验结果:

博文列表为空,说明刚才创建的博文已经被删除了。

下一步

本篇介绍了 GraphQL 后端如何实现数据库的增删改查功能。后续章节将介绍 Flutter 如何引入本篇创建的“接口”,并在前端实现博文的新增,查询等功能。

源码

代码地址

src/index.ts

import "reflect-metadata"
import { buildSchema, ObjectType, Field, ID, Resolver, Query, Ctx, Arg, Mutation } from "type-graphql";
import { ApolloServer } from "apollo-server";
import { CreateDateColumn, Entity, PrimaryGeneratedColumn, Column, createConnection, Connection } from 'typeorm';

@Entity()
class PostEntity {

  @PrimaryGeneratedColumn('uuid')
  id: string;

  @CreateDateColumn()
  created: Date;

  @Column('text')
  content: string;
}

@ObjectType()
class Post {

  @Field(type => ID)
  id: string;

  @Field()
  created: Date;

  @Field()
  content: string;
}

@Resolver(Post)
class PostResolver {

  @Query(returns => [Post])
  async posts(
    @Ctx() context: AppContext,
  ): Promise<Post[]> {
    const postRepository = context.connection.getRepository(PostEntity);
    return await postRepository.find();
  }

  @Mutation(returns => Post)
  async createPost(
    @Ctx() context: AppContext,
    @Arg('content') content: string,
  ): Promise<Post> {
    const postRepository = context.connection.getRepository(PostEntity); 
    const post = await postRepository.save({ content });
    return post;
  }

  @Mutation(returns => Post)
  async updatePost(
    @Ctx() context: AppContext,
    @Arg('postId') postId: string,
    @Arg('content') content: string,
  ): Promise<Post> {
    const postRepository = context.connection.getRepository(PostEntity); 
    await postRepository.update(postId, { content });
    return await postRepository.findOneOrFail(postId);
  }

  @Mutation(returns => String)
  async deletePost(
    @Ctx() context: AppContext,
    @Arg('postId') postId: string
  ): Promise<string> {
    const postRepository = context.connection.getRepository(PostEntity); 
    await postRepository.delete(postId);
    return postId;
  }
}

type AppContext = {
  connection: Connection,
};

async function main() {

  try {

    const schema = await buildSchema({
      resolvers: [PostResolver],
      dateScalarMode: 'timestamp'
    });

    const connection = await createConnection({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'tinylearn',
      password: 'my_password',
      database: 'tinylearn',
      synchronize: true,
      entities: [PostEntity],
    });

    const context: AppContext = {
      connection
    };

    const server = new ApolloServer({
      context,
      schema,
      playground: true
    });

    const { url } = await server.listen(4444);

    console.log(`GraphQL Playground available at ${url}`);
    
  } catch (error) {
    console.error(error);
  }
}


main();
复制代码

参考

《全栈开发 GraphQL + Flutter 最佳实践,文章系列》

《如何使用 TypeORM 接入数据库?》

《Docker 启动 MySQL 容器》

TypeORM

GraphQL

Flutter