GraphQL Resovler的执行与实践
本文首先介绍GraphQL Server对于查询的执行过程,然后结合实际开发使用GraphQL的相关特性。
GraphQL Execution
在执行查询前,GraphQL会先通过类型系统来判断查询是否有效(这里的查询涵盖query和mutation),即查询的字段是否存在、参数的类型是否正确、对象类型是否指定子字段等的规则。下图列举的是gprahql-js库中指定的全部验证规则。
验证通过之后,GraphQL就开始执行查询并返回结果。类型系统也会加入到执行阶段中。类型定义中的各个字段都有resolver(即resovle函数)来支持。当执行到该字段时,对应的resolver会被执行并返回数据。如果返回数据是标量,则该字段的执行完毕;如果返回的是对象或对象数组,查询则会继续执行直到查询到标量为止。
GraphQL Server的顶层有一个顶级类型作为查询的入口,这个类型称为Root类型或者Query类型。比如下列给出的GraphQL schema中,提供了一个查询test字段的query。
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'NetNodeInfo', //object
description: 'test infomation',
fields: {
test: {
type: GraphQLString,
description: 'string info for test',
resolve: function (parent, args, context) {
return 'query graphql successfully';
}
}
}
})
});
分析:这个test字段是String类型,它的resolve(即resolver)函数直接返回字符串。
当GraphQL resolver需要传递变量怎么办,比如客户端传递的参数提供查询条件、数据库连接提供操作数据库。GraphQL查询方法是将实体对象以层级连接的,如果上一层对象需要提供给本层revoler查询的字段,GraphQL又怎么传递这种参数呢。其实GraphQL早已将参数依据来源分为三类(这也是resolve的参数列表):
- 父类对象(parent):提供在类型系统中位于上一层的对象,能够用来关联数据。
- 查询参数(args):客户端在查询时传递的参数(query variables),用来查询用户指定的数据。
- 环境变量(context):传递给每个resolver的全局参数,用来传递比如用户凭证、数据库连接等对全体(或者说大多数)resolver都需要使用的状态。
类型系统在resolver执行时会进行优化操作,即同一层次的字段的resolver会并发执行。
当每个字段的resolve函数都被执行后,结果会从底层到顶层放置到一个key-value map中。最后这个map数据会以原始查询的结构返回到客户端。http上的GraphQL一般将结果以json格式放置在response的body发送。
Practice
首先我们需要修改在服务器传递MongoDB连接实例的方式。
在上篇文章中的实践部分已经成功读取MongoDB的数据。不过这个实例是通过rootValue传递的:rootValue: { db: req.app.locals.db }
。这个参数只会传递给schema中的第一层字段的resolver,更低层的resolver无法获得这个参数。正如我们上面所描述的,对于每个resolver都可能会用到参数最好以context的形式传递。
通过查阅上图 graphqlHTTP
的API,我们以context参数传递db实例变量。同样的,我在这里给出graph.js的API。与express-graphql有点不一样,graphql.js以contextValue传递全局变量,而express-graphql在其封装来一层,全局参数通过context参数,这个参数的默认值是express的req对象。
正如前面所说,在原来的类型定义中需要修正数据库实例的传递到resolve函数的方式。传递给 resolve
函数的参数为(parent, args, context), db
就通过 context
传递的,我们使用ES6的解构赋值给局部变量db。
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'NetNodeInfo', //object
description: 'netnode geograph infomation about faults, alarms infomation',
fields: {
test: {
type: GraphQLString,
description: 'string info for test',
resolve: (() => 'query graphql successfully')
},
netnode_info: {
type: new GraphQLList(netnode_type),
description: 'netnodes information',
args: {
nums: {
type: GraphQLInt,
defaultValue: 100
},
customer_name: {
type: GraphQLString
}
},
async resolve(parent, args, { db }) {
// in this scope, parent is undefined
// args is full with { nums, customer_name } if they send by request(nums has default value)
// context has the db instance
}
}
}
})
});
同样的,在位属下一层级的netnode_type类型定义的resovle函数也做类似处理。在这个场景下,fault_log需要netnode的netnode_id字段来查询 fault_log
集合相关数据的。而netnode_id字段能直接通过parent来拿到。
let netnode_type = new GraphQLObjectType({
name: 'netnode',
description: 'single netnode infomation',
fields: {
// some scalar fields omitted
fault_log: {
type: new GraphQLList(fault_log_type),
description: 'netnode\'s fault log information',
args: {
report_time_from: {
type: GraphQLDate,
description: 'begin of report time of query(use UTC format:YYYY-MM-DDTHH:MM:SS.SSSZ)'
},
report_time_to: {
type: GraphQLDate,
description: 'end of report time of query(use UTC format:YYYY-MM-DDTHH:MM:SS.SSSZ)'
}
},
async resolve(parent, args, { db }) {
// in this scope, parent is previous object that has netnode_id
// args is full with { report_time_from, report_time_to } if they send by request
// context has the db instance
}
}
}
});
最后我们测试一下这个接口的使用,这个我们使用查询参数和查询片段(Fragment)。假设我们需要查询“中国银行”和“交通银行”的相关netnode数据,我们可以使用查询片段来复用代码。并且,fault_log数据只需要2015年8月1日的数据。
查询语句是这么写的:
query NetNodeInfo{
boc: netnode_info(nums: 10, customer_name: "中国银行"){
...BankFragment
}
bcm: netnode_info(nums: 10, customer_name: "交通银行"){
...BankFragment
}
}
fragment BankFragment on netnode {
id
net_node_name
net_node_address
customer_name
longitude
latitude
fault_log (report_time_from: "2015-08-01T00:00:00.000Z"){
report_time
task_id
}
}
最后得出的结果如图:
由于我们使用来字段的别名(“boc”和“bcm”),查询的结果以下列的形式返回,这和查询语句的结构是一样的。
{
"data": {
"boc": [...],
"bcm": [...]
}
}
通过具体开发可以看出,GraphQL给前后端交接带来了便捷和查询的灵活性。这些查询在REST API上是不可能出现的。但是这种灵活是把双刃剑。相比简单的CRUD的API,GraphQL会增加API的复杂度。