文章原文请移步我的博客:GraphQL 接口设计

graphql 是一种用于 API 的查询语言。它提供了一套残缺的和易于了解的 API 接口数据形容,给客户端势力去精准查问他们须要的数据,而不必再去实现其余更多的代码,使 API 接口开发变得更简略高效。

最近在用Gatsby开发一版动态博客,感觉和这个框架真是相见恨晚。因为这个框架应用到了graphql技术,所以我花了点工夫去学习了一下,并且记录了一下学习和思考过程。

本文次要解说如何了解graphql以及基于关系型数据库设计graphql的思路。如果须要学习graphql基础知识,请移步官网文档

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

了解graphql

graphql是一个用于形容数据及其关系的查询语言,官网文档中形容了graphql的规范,具体的实现依附第三方,目前支流的是Apollo提供的解决方案。

graphql并不关怀具体咱们是怎么获取数据的,咱们只须要提供获取数据的办法(resolver)以及如何组装数据(schema)。相似于Java开发过程中的接口设计模式,Graphql定义了一套规范,咱们依照规范实现接口。上面以用户-角色模型举例。

上面的代码定义了两个数据结构,与JSON相似,应该很容易了解。它形容了每个类型(type)的名称,以及其蕴含的属性。属性除了能够是根本类型外,也能够是数组、其余援用类型,从而建设起所有数据模型及互相的关系图。

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

下面代码用于形容RoleUser的数据结构,那么咱们具体要怎么应用这个货色呢?先从前端的角度来看,能够从官网文档学习到前端的根本应用,申请体中的数据形容和下面定义类型的代码有些许差异,比方咱们要查问用户数据:

query userInfo {  user(id: Int) {    id    name    gender    role {      name      note    }  }}

从下面的代码能够大略猜测出,如果咱们不须要查问role数据是,只须要将其从申请中去掉

query userInfo {  user(id: Int) {    id    name    gender  }}

当申请达到服务端时,graphql会对申请体进行解析,当解析到user时,会执行咱们定义好的获取user数据的逻辑,解析到role也是同样的情理。那么咱们得在服务端定义好获取数据的逻辑,在graphql中叫做resolver

之前的代码只定义了数据的构造,咱们还须要为其创立一个resolver来获取对应的数据,相似于上面的代码

function userResolver (id) {  // ...  // return User}function roleResolver (userId) {  // ...  // return Role}

咱们能够在resolver中执行sql、http申请、rpc通信等任何可能获取到所需数据的逻辑。最终只须要依照预约义的构造将数据返回即可。

Schema

后面用来定义不同类型数据结构的代码,称做Schema。它是graphql用来形容数据结构和关系的语言,在Schema的type定义中,能够应用Int, Float, StringBoolean, ID等graphql定义的标量类型(Scalar Types),也能够应用联结类型,援用另一个type等,如下面代码User中援用了Role

Resolver

Resolver是GraphQL中用来获取数据的办法。它不关怀Resolver的具体实现,咱们能够从数据库, HTTP接口, 服务器资源等渠道获取数据。

graphql会依据申请中申明的字段来执行resolver,肯定水平上能够缩小查问次数。上面的代码中,会执行UserResolver,完结后再继续执行RoleResolver

{  User {    name    role  }}

如果咱们把role字段去掉,那么服务器将不会再执行RoleResolver

{  User {    name  }}

UserResolver执行完结后,就会将数据返回给前端。

可见graphql能够动静的执行数据查问,缩小不必要的资源耗费。然而,凡事都有双面性,从另一个角度来思考,咱们该如何管制Resolver的粒度?假如咱们的Resolver是进行数据库查问,在restful API中,通常咱们会应用一个关联查问同时获取到UserRole两个数据。然而在graphql中,咱们为每个关联对象都创立一个Resolver,当咱们在查问一个用户列表,并且须要蕴含用户相干的角色时,就会发现一个问题,一次申请须要执行N+1次sql查问:1次UserResolver获取N个用户列表,N次查问获取每个用户的角色。这就是前面须要讲的N+1问题。

graphql存在的问题

N+1问题

N+1问题并不是只存在于graphql中,咱们在写sql的时候,也会存在相似的状况,只不过咱们会通过关联查问来防止这个问题。

为了解决N+1问题,graphql官网提供了一个dataloader解决方案。dataloader应用了缓存技术,同时也会合并屡次雷同(相似)的申请,将后面讲到的N次查问合并成一次。基本原理就是先执行查问列表的操作,而后将每条记录的关联字段作为参数列表,通过一次查问拿到所有的关联数据后,再合并到下级数据中。dataloader是目前解决N+1问题比拟无效的办法,在应用上也没有太多艰难。

其次,咱们能够通过管制Resolver的粒度来缩小查问次数。比方后面的示例中,不写roleResolver,而是间接通过关联查问,用一次查问获取到UserRole。当然,这样做的话,无论前端是否查问role字段,服务都会进行关联查问,这里须要依据具体场景取舍。

HTTP缓存问题

因为graphql接口申请只有一个对立endpoint,会导致咱们无奈应用HTTP缓存。目前买一些前端实现中,如Apollo,提供了inMemeryCache解决方案,但在应用上体验不是很敌对。

关系型数据库+graphql接口设计

大略理解了graphql之后,咱们来开始进行关系型数据库(Mysql)+ graphql的API设计

编写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都是连同的,这样咱们在申请的时候,就可能从一个节点达到任意一个与之有关系的节点。

这也是我感觉graphql的一大魅力,当咱们建设起残缺的关系图后,前端能够自在的查问、组合数据。从实践上来讲,前端能够有限递归查问一组数据,如:小明->小明的敌人->小明的敌人的敌人->...,咱们只须要选定好一个终点,便能达到任何中央。

一对多关系

一对多关系的建设很简略,咱们只须要编写对应的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中,如果只应用关联查问,显然是没有充分发挥其个性的。咱们来看如下示例

# schematype 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中的实际和思考,如有谬误或倡议欢送分割我斧正和探讨。另在在实际之前该当着重思考是否须要应用graphql,因为restful api曾经能满足大部分的场景需要,自觉的去应用graphql可能会带来一些意料之外的问题。