简介
本文是《全栈开发 GraphQL + Flutter 最佳实践,文章系列》中的一员。主要介绍 GraphQL 是如何以 MySQL 作为数据源,来提供增删改查操作“接口”的:
- 查询博文
- 新增博文
- 修改博文
- 删除博文
项目依赖
-
初始项目
- 如果你已经完成《如何使用 TypeORM 接入数据库?》,可以继续使用项目代码
- 如果没有完成,可以点击这里下载初始项目
-
一个可连接的数据库
- 如果你已经完成《Docker 启动 MySQL 容器》,需确保 Docker 正常运行即可
- 如果你有其他可用数据库,下文需要自行替换连接参数
第 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 里面 Query 和 Mutation 都是入口,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();