文章原文请移步我的博客: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
}
下面代码用于形容 Role
和User
的数据结构,那么咱们具体要怎么应用这个货色呢?先从前端的角度来看,能够从官网文档学习到前端的根本应用,申请体中的数据形容和下面定义类型的代码有些许差异,比方咱们要查问用户数据:
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
, String
,Boolean
, 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 中,通常咱们会应用一个关联查问同时获取到 User
和Role
两个数据。然而在 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
,而是间接通过关联查问,用一次查问获取到User
和Role
。当然,这样做的话,无论前端是否查问 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
中蕴含了 groups
和userGroups
,同样的,在 Group
中也蕴含 users
和userGroups
。而在 UserGroup
中同时蕴含了 User
和Group
,于是,咱们能够进行如下查问
{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 可能会带来一些意料之外的问题。