Aug 09, 2024

API进化论(一):从REST到GraphQL的蜕变之路

打算写一个系列文章来沉淀下在我在“获取数据”方面的实践与思考,叫做《API进化论》,会提到GraphQL、Restful、tRPC他们解决了什么问题,帮助大家认识API的进化历程,本文是第一篇。


这也是一篇公司内布道文章,因为GraphQL在已有服务的改造成本太高,在项目内没有引入,但是其中的很多概念和开发体验还是打动了一些同事。GraphQL目前的状态不温不火,加之一些同构RPC框架的出现,开发者的选择有了更多选项。至少笔者认为GraphQL至少让接口类型安全被重视。

前言

RESTful架构的提出已经有20多年的历史了,它从资源的定义、获取、表述、关联、状态变迁等角度提供了一些很棒的想法。随着端和服务的高速发展,在它已经无法跟上访问它的客户端快速变化的需求。

本文通过分析列举了目前RESTful存在的一些问题,然后介绍GraphQL是通过何种方式解决他们,在解决这些问题过程中我们需要投入怎样的成本、如何权衡。帮助大家进一步了解GraphQL能给我们业务能带来怎样的收益。

一、简单介绍

GraphQL是一款API查询语言,和RESTful一样,是一套基于HTTP的请求规范。 GraphQL的一些优秀特性:

  • 按需索取
  • 类型规范
  • 支持内省

💡 前面提到GraphQL是一门语言,所以大多数特性需要业务方来提供,只不过社区提供很多现成的解决方案,比如Apollo GraphQL,后面也会介绍一下其他GraphQL的优秀实践方案。

使用GraphQL的公司 参考

Untitled

二、RESTful有哪些缺点

GraphQL不是凭空产生的,而是基于目前RESTful一些缺点而产生的解决方案,这能够让我们很好的去理解 GraphQL 为什么要这样做

1、获取过度与获取不足

场景

我们先假设有一个博客API的场景,获取当前用户、获取用户的文章、获取用户的粉丝。

如果使用REST API我们可能会这样去设计:

Untitled

这样看起来没什么问题,不过当我们的业务增加了一些需求,比如

  • 移动端的用户列表不需要显示 address 字段
  • 新增“我的评论”功能:需要显示用户最近的评论

我们在设计API的时候也会讨论接口的通用性、复用性,在需求变化时尽量少的变更意味着成本的节省和效率的提升。

在上面3个需求的设计过程中有时候我们不得不重新为移动端或者管理平台提供新的接口,并且随着多个端的UI变化,接口的维护变得愈加困难。

事实上这是REST最常见的问题之一:获取过度获取不足

获取过度(OverFetching):

下载了多余的没有使用到的字段数据;就比如在PC网页中,需要展示客户所有信息,客户端中只需要显示昵称。

获取不足(UnderFetching):

未提供足够的所需数据;比如管理平台需要更敏感的数据,再比如客户端需要新增一个“我的评论”功能。

获取不足还会导致你的程序需要发送多次请求才能获得充足数据,这将会使程序变慢。

端点爆炸(Endpoint ):

因为获取过度与获取不足导致后端业务侧不断增加接口(端点),导致后期维护的成本也越来越大,这种情况可称为端口爆炸。

这点尤其是在一些维护时间很长、或者营销活动的需求中更为明显。

为什么会这样?

一是RESTful的设计过于僵硬,在当前多平台、复杂业务接口的需求上。

二是资源的获取方与提供方存并不是同一个人,会在沟通gap。

2、迟滞并难以维护的文档

Untitled

文档迟滞或者说没有文档会造成的一些问题:

  • 无法预知的输入与结果
  • 联调沟通成本增加
  • 端开发者需要翻译文档,比如转成对应语言的Interface或者Struct

相对于维护代码,维护API文档是一件困难的事情,所以很多团队也会用代码生成文档的工具减少文档维护的工作量。

其主要原因是代码与文档之间同步是需要成本的,而且时间异常,冗余的部分也会随之增长,比如将业务字段的变动翻译成文档,这些都需要投入较多的成本。

3、权限控制粒度小

Untitled

当我们搭建完博客主页,需要做个博客文章管理平台时,需要显示软删除、或者敏感度较高的权重字段时,我们很少会复用之前端的博客列表接口(因为获取不足),采用新的接口来做。

主要原因是RESTful的权限粒度一般是以端点(Endpoint)为单位,灵活的RESTful字段权限控制就意味着复杂的解释文档。

三、GraphQL是怎么解决这些问题的

1、单一接口与按需查询

首先我们来看,GraphQ如何解决过度获取获取不足的。

单一接口

正如Github GraphQL API里面所描述的,Github只有一个端点https://api.github.com/graphql

💡 这里需要说明一下,GraphQL的请求全都是通过POST发送出去的,(这有点像POST一把梭),查询语句放在body里面。

这使得你只需要关心查询语句,不需要去关心接口名称。即使后端的资源发生了变化。

按需查询

数据始终是存储在数据库中,如果需要做到真正的按需查询,不考虑安全的情况下,前端直接SQL查表应该是最舒适的,虽然不能直接这样去做,但也可以给我们一些启示与灵感激发。并且下文也会通过一些SQL代码来解释一下。

GraphQL有一套API查询语言,如下图,目的是查询用户的文章列表,只需要标题,查询用户的粉丝,只显示名称。

Untitled

💡 如上面所说,query的语句是通过POST请求放在body里面发送到后端,然后后端返回对应的JSON数据回来。

如果我们不需要粉丝数据,我们完全可以去掉 followers ,这样,返回里面也不会有这个字段。当然这里需要有后端实现,下面就会讲到后端如何处理我们发送出去的query。

后端如何处理Query

我们先用上面的请求例子解释一下query发送到后端后的流程图:

Resolve:表示一种资源的处理器

RPC:微服务常用的通讯协议

当我们使用RESTful处理请求时:后端一般会给每一个API建立一个Controller,然后在Controller中集中去请求对应的Service。

Untitled

在GraphQL中,当我们把user、post、follower的请求发送给GraphQL后,GraphQL能够识别到这个query请求了3种资源,然后分别交由对应的Resolve去处理,处理后,在GraphQL会将结果按照Query的结构聚合结果,然后通过Response返回。

这样的方式使得聚合更加灵活,在3个资源中可以做到任意组合,甚至可以做到请求粉丝下的粉丝。

query GetFollwersDeep{
	User{
		followers{
			name
			followers{
				name
			}
		}
	}
}

在一些需求场景,UI的变动后导致数据结构变化,前端可以直接修改Query结构,后端不必投入成本开发!

Golang Resolve演示

下面我通过Go语言来简单实现演示一下Resolve如何处理,我手动把上面的query转换成SQL语句来演示一下,这里不包含业务逻辑,业务逻辑需要是现房来决定,只做单纯的查询。

type Query struct {
	user *User
	posts []*Post
	followers []*Follower
}

type User struct {
	ID string
	Name string
}

type Post struct {
	ID string
	Title string
	Author *User
}

func (q *Query) User(args struct{ID string}) *User {
	// 根据ID查询用户信息并返回
	// select name,id from user_table where id = ID;
}

func (q *Query) Posts(args struct{AuthorID string}) []*Post {
	// 根据AuthorID查询该用户的所有文章并返回
	// 返回 select title,id form post_table where user_id = AuthorID;
}

func (q *Query) Followers(args struct{UserID string}) []*User {
	// 根据UserID查询该用户的所有粉丝并返回
	/* 返回
		SELECT user_table.name
		FROM user_table
		JOIN user_followers_table ON user_followers_table.follower_id = user_table.id
		WHERE user_followers_table.user_id = UserID
	*/
}

可以看到,和以往不同的是,我们将资源分开去处理,然后通过GraphQL去聚合,而不是直接通过SQL的联表查询去实现。

这里很好的适配了微服务的调用方式,因为在微服务当中,我们很难去做跨表联表查询,通常是根据Protocol Buffers通过RPC去调用,这很契合GraphQL的想法,也使得GraphQL在BFF层接入变得更加容易。

请求数据并不天生就是聚合的,而是后端服务根据UI或者业务场景帮我们聚合的(或者前端BFF层来实现聚合),GraphQL就是通过这样的方式将聚合的控制权交给前端。这样无论UI如何变化,我们都能通过Schema(下面会将)的关系来获取前端想要的数据,解决了过度获取与获取不足的问题

2、类型规范

这里聊一下GraphQL怎么通过类型规范以及内省解决迟滞并难以维护的文档、翻译文档成本的问题。

GraphQL Schema

类型规范对代码质量和团队协作效率的提升是显而易见的,通常RESTful接口变量的输入输出是需要靠约定来规范的,并且这份约定需要人为的去维持,一旦新增或者字段格式变化,都需要手动去更新这份约定,在复杂系统里,这里较高的维护成本是存在的。

GraphQL输入与输入都有对应的Schema规范,需要提前定义(或者通过工具生成),这点和Protocol Buffers有点相似,事实上你也可以 通过。PB Schema转成GraphQL Schema。

比如上面的请求,我们定义一下其GraphQL Schema

type User {
	id: ID! // 唯一ID
	name: String! // 名称
	posts: [Post]
	followers: [User]
}

type Post {
	id: ID!
	title: String!
	body: String!
	author: User!
}

type Followers {
	id: ID!
	user: User!
	follower: User!
}

内省-接口即文档

Introspection

有了Schema之后,这意味着,可以让我提前知道服务能够提供哪些接口,接口的输入限制与输出结果的格式。

GraphQL通过 __type 暴露其所有的Schema(这里暴露的范围可以通过权限控制),并且可以携带对应资源与字段的 Comment

Untitled

这个是一份GraphQL的调试工具,我们一栏一栏(从左到右)的去解释

  1. 第一栏:这里列举了所有的Query资源
  2. 第二栏:这里可以写query语句,并且有自动完成
  3. 第三栏:语句返回的结果
  4. 第四栏:语句的输入类型,返回类型,字段描述等等

不仅如此,这份Schema可以转换成各种语言的类型系统,比如TS、Swift等等。

同样,在开发体验上也有很多提效工具,比如IDE自动订阅最新的GraphQL Schema,通过查询语句转换成对应输入输出的类型,这部分会在另一篇前端文章中讲到,很推荐前端同学看看GraphQL在前端中的开发体验!

3、更原子化的字段权限控制

讲到这里其实已经不难理解GraphQL是如何去控制字段权限了。

简单说就是,我们在Resolve中去判断用户是否有字段权限,然后给与404或者400。

更细的权限粒度控制使得GraphQL的端点能用到所有的终端,包括管理台接口。

4、其他优点

  • 优秀的生态
  • mock成本变低 UI变更带来的接口更替成本 多端接口
  • 市面成熟的解决方案较多

下面是使用GraphQL的一些公司,在搜索引擎中有很多他们的实践经验。

GraphQL Landscape

四、权衡

使用GraphQL会和以往的开发模式有些差异,并且也会带来一些改造成本,并不是每个业务都需要接入GraphQL,这里首先会列举一些使用场景,如果没有这样的痛点,其实大可不必引入GraphQL。

因此,你需要看带来的成本能否动摇你使用GraphQL的信心;这里笔者也针对一些问题进行调研,并提供了一些解决思路,需要读者自己去研究和决策是否能够使用在自己的业务上。

毕竟GraphQL不是银弹,抛开场景谈技术都是耍流氓。

1、适用场景

适合的场景

  • 端需要访问多个不同数据源:如REST API、数据库和微服务等。GraphQL可以通过定义类型和字段来统一聚合这些数据源,避免编写多个不同的接口。
  • 端的类型比较多:如小程序、APP、PC、H5,用户需要自定义查询字段和参数。GraphQL的查询语法可以满足这种需求,而REST API的静态资源定义往往无法满足。
  • 新业务:新业务的技术债和改造成本是比较低的,比较推荐。

不适合的场景

  • 仅需要访问一个数据源:这种情况下,REST API可能更适合,因为GraphQL的类型定义和查询语法带来的额外复杂度可能不值得。
  • 端接口功能比较简单:应用程序的数据源仅需要提供基本的增删改查功能。GraphQL的类型定义和查询语法可能比REST API的资源定义更加复杂,在这种情况下使用GraphQL可能会增加开发难。

2、基建需要适配

在实践过程中,数据上报与监控视图是必须要适配的一项。

因为GraphQL是单端点,日志上报的请求都是来自同一个端点,这个会给排查问题带来困难。

这里引用下爱奇艺技术产品团队的一个解决思路:

GraphQL的query是可以命名的

query getabc{
	User{
		userName:user_name
	}
}

在上报的时候,可以将这个名字直接追加到请求url上面

Untitled

在监控视图中也就有对应名称的监控服务了

Untitled

3、学习成本

这里的学习成本包含两部分,一是语言学习成本,二是设计成本。

  • 语言学习成本:这个易理解,需要了解GraphQL的请求语法和Schema定义方式。
  • 设计成本:GraphQL好不好取决于对查询的设计,比如上面查询是查询到了我所需要的对象,但是查询不止于此,除了列表查询,我们还需要个体资源查询,统计等。至于如何设计,我们将这部分放到另一篇后端文章来讲解。

小结

这里简单总结一下本文的核心内容

我们简单介绍了一下GraphQL的定义和优秀特性,了解了GraphQL是一个比较成熟的解决方案,在较多大厂中都有相关的实践。

然后我们简单列举了目前RESTful的现状、缺点,以及分析其出现的原因,让大家能够身临其境,激起大家对问题的思考。

接着我们通过一些特性了解到GraphQL是如何解决RESTful的一些问题,并且提供了图文帮助大家理解我们现有的后端如何去改造。

最后,我们再次回到现实,了解GraphQL会给我们带来的成本,并且简单列举了一些使用场景。

希望通过阅读这篇文章你能让你有所受益。