2020-03-25 7 技术
GraphQL 接口设计

GraphQL 是一种为 API 接口和查询已有数据运行时环境的查询语言。它提供了一套完整的和易于理解的 API 接口数据描述,给客户端权力去精准查询他们需要的数据,而不用再去实现其他更多的代码,使 API 接口开发变得更简单高效,支持强大的开发者工具。

本文主要讲解如何理解GraphQL以及基于关系型数据库设计GraphQL的思路。如果需要学习GraphQL基础知识,请移步官方文档

本文包含Nestjs + GraphQL的示例项目:https://github.com/YES-Lee/nestjs-graphql-starter

理解GraphQL

GraphQL是一个用于描述数据及其关系的查询语言,官方文档中描述了GraphQL的标准,具体的实现可以使用Apollo

GraphQL并不关心具体我们是怎么获取数据的,我们只需要提供获取数据的方法(resolver)以及如何组装数据(schema)。比如我们要获取如下数据:

type Role {
  name: String
  note: String
}
type User {
  name: String
  gender: Int
  role: Role
}

上面代码描述了RoleUser的数据结构以及两者的关系,那么我们可以分别写如下resolver去获取这些数据:

function userResolver () {
  // ...
  // return User
}

function roleResolver (userId) {
  // ...
  // return Role
}

Schema

SchemaGraphQL用来描述数据结构以及数据间关系的语言,在Schematype定义中,可以使用GraphQL中的基本类型,也可以引用另一个type。如上面代码User中引用了Role

Resolver

ResolverGraphQL中用来获取数据的方法。GraphQL不关心Resolver的具体实现,我们可以从数据库, HTTP接口, 服务器资源等渠道获取数据。

GraphQL会根据前端发送的请求决定是否执行某个Resolver,如前面的代码中,如果用户的查询如下:

{
  User {
    name
    role
  }
}

此时,查询请求中包含了role字段,GraphQL将会去执行roleResolver方法获取角色数据。如果查询是:

{
  User {
    name
  }
}

此时,GraphQL不回去调用roleResolver方法获取角色。

GraphQL存在的问题

N+1问题

前面讲到了Resolver的执行时机,会存在一个性能问题,当我们请求的数据是长度为N的User的列表,其中每一个都包含了roleGraphQL就会进行一次UserList的查询,以及针对每个UserRole查询,总共会进行N+1次查询。这个问题叫做N+1问题。

为了解决N+1问题,GraphQL官方提供了一个dataloader的库来解决这个问题。dataloader的解决方案是使用缓存和合并多次相同(类似)的请求。其次,我们可以通过控制Resolver的粒度来减少查询次数。比如前面的示例中,不写roleResolver,而是直接通过关联查询,用一次查询获取到UserRole。当然,这样做的话,无论前端是否查询role字段,服务都会进行关联查询,这里需要根据具体场景取舍。

HTTP缓存问题

由于GraphQL接口请求只有一个统一的入口,会导致我们无法使用HTTP缓存。目前一些前端框架如Apollo,实现了inMemeryCache,而个人觉得并不是很好用。

关系型数据库GraphQL接口设计

讲完了GraphQL的一些概念和存在的问题,如果读者还不能对GraphQL有一个清晰的认知,那可能是我写的还不够通俗易懂,可以通过我的联系方式进行交流。

编写Schema

对于schema的设计,首先忽略表之间的关系,之间建立起于数据表的对应模型。如User表和Role表,分别建立如下schema

type User {
  name: String
  gender: Int
  # password: String // 敏感信息不应该出现
}
type Role {
  name: String
  note: String
}

其中需要注意的是,敏感数据不应该出现在敏感信息(用户密码等),即使Resolver的结果包含这些敏感信息,只要schema中没有包含,GraphQL会自动过滤这些字段。

建立好所有的表对应的schema之后,再来考虑表之间的关系。

表关系的处理

GraphQL对于关系的处理于Restful API有一些区别,在Restful API中,我们一般只在有需求的接口中建立关系查询,我们会针对接口做一些SQL优化,以求在一条SQL中能够查询出所需要的一切信息。

但是在GraphQL中,我们应当把所有表的关系描述为一个图结构,保证所有有关系(一对多或多对多)的表对应的schema都是连同的,这样我们在请求的时候,才能够从一个表到达任意一个与之有关系的表。

这里就要使用到前文提到的Resolver特性,同时也会存在前文提到的N+1问题。

一对多关系

一对多关系的建立很简单,我们只需要编写对应的Resolver,然后在主表的schema中添加字段即可

type Role {
  name: String
  note: String
}
type User {
  name: String
  gender: Int
  role: Role
}
function userResolver () {
  // ...
  // return User
}

function roleResolver (userId) {
  // ...
  // return Role
}

我们写了roleResolver之后,在User中添加了role字段,当请求该字段时,GraphQL会去执行roleResolver来获取数据,下面来看多对多关系的处理。

多对多关系

通常,在Restful API中,我们会通过一条SQL关联查询,获取多对多的关联数据,但是在GraphQL中,如果只使用关联查询,显然是没有充分发挥期特性的。我们来看如下示例

# schema
type User {
  name: String
  gender: Int
  role: Role
  groups: [Group] # 用户组
  userGroups: [UserGroups] # 用户组关系表
}

type Group {
  id: Int
  name: String
  note: String
  users: [User]
  userGroups: [UserGroups]
}

type UserGroup {
  id: Int
  userId: Int
  groupId: Int
  note: String # 关系表中存储一些关联信息
  user: User
  group: Group
}

对于上面的schema,可以看到,在User中包含了groupsuserGroups,同样的,在Group中也包含usersuserGroups。而在UserGroup中同时包含了UserGroup,于是,我们可以进行如下查询

{
  user (id: 1) {
    name
    groups {
      id
      name
      note
      userGroups {
        id
        userId
        groupId
        note
        user {
          name
          groups {
            # ...
          }
        }
      }
    }
  }
}

可能有人会问,上面的操作无限循环了。没错,确实无限循环了,这并不是bug,而是我前面提到的建立起了连同关系。对于不同的场景,我们可以进行不同方式查询,比如当我需要对用户的用户组进行搜索的时候,我可以在groups中添加一些参数

{
  user (id: 1) {
    name
    groups (name: "admin") {
      id
      name
      note
      userGroups (userId: 1) {
        id
        note
      }
    }
  }
}

上面的查询,如果我们只想对UserGroup关系表中的额外信息进行搜索时,上面的查询方式可见是行不通的。那么我们可以从另一个方向进行查询

{
  user (id: 1) {
    name
    userGroups (note: "新用户") {
      id
      userId
      groupId
      note
      group {
        id
        name
        note
      }
    }
  }
}

可以发现,通过建立了对应关系的连通图之后,我们可以从一个表查询到任意一个与之关系的表,同时可以无限嵌套查询。

对于无限循环问题无需担心,因为我们需要指定关联字段后,GraphQL才会去执行对应的Resolver,要想出现死循环,除非我们的查询也无限循环的写下去,显然这是不可能的。

结束语

本文也是我在学习和使用GraphQL中的经验和思考,如有错误或意见欢迎联系我指正和探讨。