关于graphql:GraphQL-接口设计

61次阅读

共计 4673 个字符,预计需要花费 12 分钟才能阅读完成。

文章原文请移步我的博客: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 可能会带来一些意料之外的问题。

正文完
 0