GraphQL Resovler的执行与实践

2016-11-30
5 min read

本文首先介绍GraphQL Server对于查询的执行过程,然后结合实际开发使用GraphQL的相关特性。

GraphQL Execution

在执行查询前,GraphQL会先通过类型系统来判断查询是否有效(这里的查询涵盖query和mutation),即查询的字段是否存在、参数的类型是否正确、对象类型是否指定子字段等的规则。下图列举的是gprahql-js库中指定的全部验证规则。

node lib:graphql-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的形式传递。

express-graphql api

通过查阅上图 graphqlHTTP 的API,我们以context参数传递db实例变量。同样的,我在这里给出graph.js的API。与express-graphql有点不一样,graphql.js以contextValue传递全局变量,而express-graphql在其封装来一层,全局参数通过context参数,这个参数的默认值是express的req对象。

graphql.js api

正如前面所说,在原来的类型定义中需要修正数据库实例的传递到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
    }  
}

最后得出的结果如图:

result

由于我们使用来字段的别名(“boc”和“bcm”),查询的结果以下列的形式返回,这和查询语句的结构是一样的。

{
  "data": {
    "boc": [...],
    "bcm": [...]
  }
}

通过具体开发可以看出,GraphQL给前后端交接带来了便捷和查询的灵活性。这些查询在REST API上是不可能出现的。但是这种灵活是把双刃剑。相比简单的CRUD的API,GraphQL会增加API的复杂度。


References:

  1. graphql-js的execute

  2. express-graphql的Options

  3. Graphql的别名和片段

comments powered by Disqus