使用Node.js实现GraphQL API
首先
我将在Node.js中实现GraphQL API。关于GraphQL的概述以及客户端模式的描述,我看到了一些日文文档。然而,关于API实现的文档却不太常见,所以我会写下来。
我将介绍我自己进行研究并实施的样例。如果您能参考其中一例,我将感到非常幸运。如果您有更好的实施方法,请告诉我。
不提及GraphQL的概述。官方网站和下面的文章已经介绍了相关内容。(第二篇文章的后半部分还介绍了在Ruby中进行API实现的方法。)
-
- GraphQL入門 – 使いたくなるGraphQL
- 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ
制作物(概要设计)
语言框架
-
- TypeScript
-
- Node.js
- express
我們將在Node.js的Web框架express上創建一個運行的GraphQL API。
组成
构成如下图所示。MySQL被设置在数据库中,GraphQL API对MySQL进行包装。API将GraphQL请求转换为常规的SQL查询,并向数据库发出查询,并将结果作为响应返回。

题材
假设有一个电商网站的数据库。这个数据库按照以下结构进行管理:客户信息、产品信息、制造商信息和订单信息。假设所有的表都有一个自增的主键”id”。
客户信息 (users)
产品信息
制造商信息
订单信息 (订单)
订单信息和产品信息的连接表(order_product)
这是一个用于合并上述orders和products的表格,用于记录每个订单中所购买的商品。
模式定义
以下是将上述表结构替换为GraphQL模式定义的结果。请注意,这里只是部分内容。您可以在github.com上查看完整定义。(这些定义是从JavaScript实现中自动生成的。)
"""お客様の情報"""
type User {
id: ID
name: String
"""性別"""
gender: Gender
"""会員ランク"""
rank: MemberRank
orders: OrderConnection
}
"""商品"""
type Product {
id: ID
"""商品名"""
name: String
"""型番"""
modelNumber: String
"""販売価格"""
price: Int
maker: Maker
}
"""製造業者"""
type Maker {
id: ID
name: String
products: ProductConnection
}
"""注文情報"""
type Order {
id: ID
"""注文日時(UNIX TIME)"""
orderDate: Int
"""注文したユーザ"""
user: User
"""注文に含まれた商品"""
products(limit: Int, offset: Int = 0): ProductConnection
}
此外,为用户的等级和性别定义了以下枚举类型。
"""性別"""
enum Gender {
"""男性"""
male
"""女性"""
female
"""その他"""
other
}
"""会員ランク"""
enum MemberRank {
"""一般会員"""
general
"""ゴールド会員"""
gold
"""プラチナ会員"""
platinum
}
关于 “连接”
GraphQL中有一种被称为Connection的规范。虽然听起来很复杂,但实质上就是分页功能。它可以通过数据库的limit、offset来限制查询结果数量,或者设置开始显示的位置。
另外,除了通过数字指定偏移量来确定起始位置之外,还可以通过游标来确定起始位置的方法。游标指定是指在数据库的每一行中设置主键(如哈希值),并使用该值来确定起始位置的方法。这对于处理总是不断添加新行的数据非常有效。例如,Twitter和Facebook的时间线总是在不断添加新行。由于使用偏移量来确定位置会导致行的位置不断更新,因此无法指定目标行。在这种情况下,可以使用游标以行的值本身来确定位置。
请参考以下内容,介绍了有关GraphQL连接的信息。
-
- Pagination(公式サイト)
- GraphQL入門 – 使いたくなるGraphQL#より実践的な使い方
为了简化,我们将使用偏移量(offset)和限制(limit)来实现分页功能。
在上述模式定义中,XxxxxConnection代表分页功能。
例如,制造商在产品中定义了一个名为ProductConnection的项目。
"""製造業者"""
type Maker {
id: ID
name: String
products: ProductConnection
}
以下是对此的查询:
制造商(maker)创建的产品组(products),查询从第3到第10个项的结果。
边(edges)和节点(node)是规范的术语。
query {
makers {
id,
name,
products(limit:10, offset: 3) {
edges {
node {
id,
name
}
}
}
}
}
实际上,如果要实现Connection,我们可以利用一个叫做graphql-relay的库提供的一些方便的函数。
实施
现在,让我来介绍一下我们已经实际实施的部分。
代码库
这次,我已经将我自己制作的内容上传到了以下的Github存储库中。如果对您有所帮助,我将不胜荣幸。
- https://github.com/hiroyky/graphql_api_sample
在中文中,你可以这样表达”express和GraphQL”:
Express和GraphQL
首先,将包含Nodejs入口点的app.ts和包含/graphql入口点的graphql.ts放在此处。
app.ts的中文释义
启动处理本身与常见的使用express框架创建的Web应用程序相同。
使用app.use(“/graphql”, “…”)设置了GraphQL的路由。
此外,还进行了数据库连接处理。MysqlDriver是一个类,用于连接MySQL等数据库。
(省略了断开连接处理的说明。)
import bodyParser from "body-parser";
import express from "express";
import http from "http";
import graphql from "./graphql";
import MysqlDriver from "./drivers/mysql-driver";
const port = process.env.PORT || 3000;
const app = express();
const mysqlDriver = new MysqlDriver();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// /graphqlのパスに対してGraphQLへのルーティングを設定
app.use("/graphql", (req, res) => graphql(mysqlDriver)(req, res));
// 初期化処理完了時の処理
// HTTPリクエストを受け付けるようにする.
app.on("ready", () => {
app.listen(port, () => {
console.log("ready");
});
});
http.createServer(app);
// 初期化関数が完了したらreadyイベントを発火する.
init().then(() => app.emit("ready"));
// 初期化処理
// MySQLへの接続を行っている.
async function init() {
await mysqlDriver.open({
host: process.env.DB_HOSTNAME,
user: process.env.DB_USERNAME,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT as string),
});
}
这个文件是graphql.ts。
express-graphql中的graphqlHTTP()函数返回为GraphQL的入口点,接受用于schema和MySQL连接的驱动类实例db作为参数。
通过在context中设置对象,可以直接将其传递给后续提到的resolve函数。
在resolve函数中,将需要使用的对象等设置在context中。
import graphqlHTTP from "express-graphql";
import MySqlDriver from "./drivers/mysql-driver";
import schema from "./schema/schema";
export default function getGraphQLHttp(db: MySqlDriver) {
return graphqlHTTP(() => ({
graphiql: true,
schema,
context: { db },
}));
}
模式的定义 de
好的,现在让我们实现GraphQL的架构定义吧。
schema.ts的汇编。
首先,在schema.ts文件中定义了架构的框架。在这个文件中,分别设置了query(参考系)和mutation(更新系)的定义。将所有的定义都写在schema.ts文件中也没有问题,但为了可读性的考虑,我们将其分成了几个部分。在这里,我们将导出的schema设置为前进的graphqlHTTP()的schema。
import { GraphQLObjectType, GraphQLSchema } from "graphql";
import mutations from "./mutations";
import queries from "./queries";
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: "GraphQLSampleQueries",
fields: () => ({
...queries,
}),
}),
mutation: new GraphQLObjectType({
name: "GraphQLSampleMutations",
fields: () => ({
...mutations,
}),
}),
});
export default schema;
查询.ts
在queries.ts中实现查询(参照系)的定义。请注意,这里只提供了用户信息(User)和产品信息(Product)的定义,并省略了其他定义。请查看Github上的代码实现其他定义。
对于用户、用户群、产品、产品群,分别设置如下内容:
– 在type中设置资源的类型定义。
– 在args中设置可以用于获取和更新资源时的参数定义。
– 在resolve中设置用于获取该资源的函数。
用户,为了准备单数和复数形式的情况,
– 在单数形式中,使用主键返回一行数据。
– 在复数形式中,返回多行匹配的结果。
此外,在复数形式中,使用前面提到的连接设置了适用于分页的类型。
import { resolveProduct, resolveProducts } from "../resolvers/product-resolver";
import { resolveUser, resolveUsers } from "../resolvers/user-resolver";
import { IdArgument, ProductArgment, UserArgument } from "./argument";
import {
Product,
ProductConnection,
User,
UserConnection,
} from "./query-type";
const userQueries = {
user: {
type: User,
args: IdArgument,
resolve: resolveUser,
},
users: {
type: UserConnection,
args: UserArgument,
resolve: resolveUsers,
},
};
const productQueries = {
product: {
type: Product,
args: IdArgument,
resolve: resolveProduct,
},
products: {
type: ProductConnection,
args: ProductArgment,
resolve: resolveProducts,
},
};
export default {
...userQueries,
...productQueries,
};
根据这个定义,例如当请求以下查询时,设置在users中的resolve函数:resolveUsers()将被执行,并且其返回值将作为API的响应。
query {
users(gender:"female", limit:10, offset:0) {
name
}
}
突变.ts
在mutations.ts中实现mutation(更新类)的定义。写法与query(参考类)相同。
import { GraphQLNonNull } from "graphql";
import { createUser, updateUser } from "../resolvers/user-resolver";
import { UserInsertArgument, UserUpdateArgument } from "./argument";
import { User } from "./query-type";
const userMutations = {
createUser: {
type: User,
args: {
user: {
type: new GraphQLNonNull(UserInsertArgument),
},
},
resolve: createUser,
},
updateUser: {
type: User,
args: {
user: {
type: new GraphQLNonNull(UserUpdateArgument),
},
},
resolve: updateUser,
},
};
export default {
...userMutations,
};
通过这样做,可以执行以下查询。执行与createUser()函数关联的resolve函数,该函数将执行数据库的写入操作。并且通过返回写入后的值来创建响应。
mutation {
createUser(
user: {
name:"原田",
gender: "female"
}
) {
id, name, gender
}
}
模式定义(类型定义)
我们先来实现通过type指定的类型定义,比如User、Product等等。这些定义需要在query-type.ts文件中进行编写。
另外,在这些定义中使用到的枚举类型可以单独分离到enum-type.ts文件中进行编写。(虽然没有必要进行分离,但目前来看这样可以提高可读性)
查询类型.ts (Zhǐ
在GraphQLObjectType中定义了类型。然后定义了以下内容。结构体的定义和所做的工作是相同的。
– name: 类型名称
– description: 类型描述(可选的)
– fields: 类型的内容
另外,在后半部分,我们定义了用于实现分页导航的Connection类型。我们可以使用graphql-relay提供的connectionDefinitions()轻松实现它。
import { GraphQLID, GraphQLInt, GraphQLObjectType, GraphQLString } from "graphql";
import { connectionDefinitions } from "graphql-relay";
import { resolveByParentMakerId } from "../resolvers/maker-resolver";
import { resolveProductsByRelatedOrder } from "../resolvers/product-resolver";
import { resolveUserByParentId } from "../resolvers/user-resolver";
import { ConnectionArgument } from "./argument";
import { GenderEnum, MemberRank } from "./enum-type";
export const User = new GraphQLObjectType({
name: "User",
description: "お客様の情報",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
gender: { type: GenderEnum, description: "性別" },
rank: { type: MemberRank, description: "会員ランク" },
orders: { type: OrderConnection },
}),
});
export const Product = new GraphQLObjectType({
name: "Product",
description: "商品",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString, description: "商品名" },
modelNumber: { type: GraphQLString, description: "型番" },
price: { type: GraphQLInt, description: "販売価格" },
maker: {
type: Maker,
resolve: resolveByParentMakerId,
},
}),
});
export const Maker = new GraphQLObjectType({
name: "Maker",
description: "製造業者",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
products: { type: ProductConnection },
}),
});
export const Order = new GraphQLObjectType({
name: "Order",
description: "受注情報",
fields: () => ({
id: { type: GraphQLID },
orderDate: { type: GraphQLInt, description: "受注日時(UNIX TIME)" },
user: {
type: User,
description: "注文したユーザ",
resolve: resolveUserByParentId,
},
products: {
type: ProductConnection,
args: ConnectionArgument,
description: "注文に含まれた商品",
resolve: resolveProductsByRelatedOrder,
},
}),
});
export const { connectionType: UserConnection } = connectionDefinitions({
name: "User",
nodeType: User,
});
export const { connectionType: ProductConnection } = connectionDefinitions({
name: "Product",
nodeType: Product,
});
export const { connectionType: MakerConnection } = connectionDefinitions({
name: "Maker",
nodeType: Maker,
});
export const { connectionType: OrderConnection } = connectionDefinitions({
name: "Order",
nodeType: Order,
});
枚举类型.ts
在enum-type.ts文件中定义了枚举类型。
import { GraphQLEnumType } from "graphql";
export const GenderEnum = new GraphQLEnumType({
name: "Gender",
description: "性別",
values: {
male: { value: "male", description: "男性" },
female: { value: "female", description: "女性" },
other: { value: "other", description: "その他" },
},
});
export const MemberRank = new GraphQLEnumType({
name: "MemberRank",
description: "会員ランク",
values: {
general: { value: "general", description: "一般会員" },
gold: { value: "gold", description: "ゴールド会員" },
platinum: { value: "platinum", description: "プラチナ会員" },
},
});
Argument的定义
让我们先来实现基于`args`参数指定的Argument的定义。即使指定了未在此处定义的值,也会被忽略。
下面的查询中,指定了`gender: “male”`的部分。
query {
users(gender: "male") {
id, name
}
}
争论.ts
在argument.ts中,我们编写了Argument的类型。在这里,我们只描述了User和Product,其他内容被省略了。您可以在GitHub上的代码中查看所有的定义。
在此,根据前面所述,我们将定义的”Argument”分别设置给对应的args。
– UserArgument将设置给user和users的args。
– UserInsertArgument将设置给createUser的args。
– UserUpdateArgument将设置给updateUser的args。
– ProductArgument将设置给product和products的args。
import { GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLNonNull, GraphQLString } from "graphql";
import { GenderEnum, MemberRank } from "./enum-type";
const RangeIntType = new GraphQLInputObjectType({
name: "RangeInt",
fields: () => ({
min: { type: GraphQLInt },
max: { type: GraphQLInt },
}),
});
export const IdArgument = {
id: { type: GraphQLID },
};
export const ConnectionArgument = {
limit: { type: GraphQLInt },
offset: { type: GraphQLInt, defaultValue: 0 },
};
export const UserArgument = {
...IdArgument,
...ConnectionArgument,
name: { type: GraphQLString },
gender: { type: GenderEnum },
rank: { type: MemberRank },
};
export const UserInsertArgument = new GraphQLInputObjectType({
name: "UserInputArgument",
fields: () => ({
name: { type: new GraphQLNonNull(GraphQLString) },
gender: { type: new GraphQLNonNull(GraphQLString) },
rank: { type: GraphQLString },
}),
});
export const UserUpdateArgument = new GraphQLInputObjectType({
name: "UserUpdateArgument",
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: GraphQLString },
gender: { type: GraphQLString },
rank: { type: GraphQLString },
}),
});
export const ProductArgment = {
...IdArgument,
...ConnectionArgument,
name: { type: GraphQLString },
modelNumber: { type: GraphQLString },
price: { type: RangeIntType },
};
解决函数的实现
我将实现之前定义的模式中设置的resolve函数。这个resolve函数将与MySQL进行查询,并将其返回值作为响应。由于后端数据库是MySQL,所以以下是处理方式。
-
- 根据args的内容生成适用于MySQL的SQL语句。
-
- 进行MySQL查询(参考、更新)。
-
- 将查询结果适应GraphQL模式进行处理。
- 作为返回值返回。
当然的是,如果后端数据存储方式不同,就需要编写相应的处理方法。无论如何,resolve函数将执行数据的获取和更新操作,并以符合模式定义的格式返回值。
resolve函数接受三个参数,如下所示。
function resolveUsers(parent: any, args: any, context: any) {
}
-
- parent: 親となっているresolve関数の戻り値
-
- args: ユーザがGraphQLの引数の値
- context: graphqlHTTP()のcontextで指定した値
让我们考虑一个关于产品信息查询的例子。在下面的查询中,我们以id: 312作为参数,请求相应产品的id、名称和制造公司的前置条件。
query {
product(id: 312) {
id,
name,
maker {
name
}
}
}
根据前景的规划,我们设定产品如下。
product: {
type: Product,
args: IdArgument,
resolve: resolveProduct,
}
const Product = new GraphQLObjectType({
name: "Product",
description: "商品",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString, description: "商品名" },
modelNumber: { type: GraphQLString, description: "型番" },
price: { type: GraphQLInt, description: "販売価格" },
maker: {
type: Maker,
resolve: resolveMakerByParentMakerId,
},
}),
});
在这种情况下,将调用resolveProduct()和resolveMakerByParentMakerId()这两个函数。后者是嵌套在Product中的。另外,我已在最初的graphql.ts文件中设置了以下上下文。
graphqlHTTP(() => ({
graphiql: true,
schema,
context: { db },
}));
因此,传递给resolve函数的三个参数的值分别如下。
-
- resolveProduct
parent: なし
args: { id: 312 }
context: { db: db },
resolveMakerByParentMakerId
parent: resolveProductの戻り値
args: なし
context: { db: db }
在上下文中,設定的{db}將直接傳入。因此,我們應該在一開始就將共用於所有解析函數的值通過上下文傳遞。
请注意,由于需要远离GraphQL的介绍并且涉及数据库查询等内容,因此本文省略了对resolve函数的具体描述。建议您参考Github上的代码。
最终
我用Node.js实现了GraphQL的API,并将代码上传到了Github(https://github.com/hiroyky/graphql_api_sample)。