文章原文请移步我的博客: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中,如果只应用关联查问,显然是没有充分发挥其个性的。咱们来看如下示例
# 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
中蕴含了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可能会带来一些意料之外的问题。