关于graphql:GraphQL-接口设计

文章原文请移步我的博客: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中,如果只应用关联查问,显然是没有充分发挥其个性的。咱们来看如下示例

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理