Rails GraphQL VS RESTful,大佬们上 GraphQL 了吗?

lanzhiheng · 2021年04月13日 · 最后由 kalylcie 回复于 2023年12月05日 · 3447 次阅读

这篇文章简单来总结一下,这些天调研 GraphQL 的感受,以及它跟 RESTful 的优缺点对比,顺便征求一下社区大佬们的意见,你们给自己家的项目上 GraphQL 了吗?原文链接: https://www.lanzhiheng.com/posts/graphql-vs-restful


契机

近期公司的业务需要扩张,准备要做 APP,另外就是 ActiveAdmin 所写的后台已经让自家人多少有些不适,也是有重写的打算。跟前端小伙伴讨论是否需要前后端分离的时候,前端小伙伴强烈要求分离,理由如下

  1. Ruby 不好招人。
  2. 分离之后彼此之间的工作不会有太多的冗余,可以分仓库管理。

我记得社区里面有人针对这个事情回答过

自己项目的话不分离,公司的项目果断分离,因为自己没时间写。

既然如此,那还是分离吧。不过分离之余又面临另一个抉择。我们是要沿用 Rails 官方拥抱的 RESTful 模式的 API,还是尝试一下近几年兴起的 GraphQL?这是一个问题。抛开后台不谈,如果要在 APP 上尝试 GraphQL 的话,那么之前 RESTful 写给小程序的东西可能得重新写一遍。这....还是得多想想,下面来看看 GraphQL 所带来的好处是否值得我们花这个时间去折腾。

GraphQL 说它啥都比 RESTful 要好?

似乎每一个新技术的出现都会在文档里面鼓吹自己有多好,别人有多不好。先来看看 GraphQL 有多好吧。

1. 减少请求量

相比起 RESTful,即便要获取多个不同资源的数据,前端只需要调整自己的查询字符串就能只通过一个请求获取对应的数据。假设我一个页面只需要文章Post的数据,以及数据库里面所有的分类Category的数据,还有当前用户的数据那么我们可以直接这样去查询

query-result.png

前端拿到数据之后可以自己去解析,原来需要三个请求去完成的工作现在一个请求就搞定了,这咋一看还是蛮吸引的。前端的同学估计已经受够了一个页面(特别是首页)要调用十几个请求,并且还要写十几个回调函数去解析数据的日子了。

GraphQL 这套查询方式正好解决了他们这个难处,只要后端定义好相关的动作,他们只需要适当组装一下请求字符串,就能够获取自己需要的所有资源数据,并且只需要向服务器请求一次。比起要请求服务器多次的 RESTful 模式,这种方式固然节省了不少的请求量,请求量少了,页面加载速度自然要快一些,这点无可厚非。咱们暂不反驳,先继续往下看。

2. 自定义数据的返回,节省数据传输量

官方说,利用 GraphQL 前端可以自定义数据的返回,后端只需要前期做好定义,后期不用做任何改动。一般来说前端通过接口能获得什么数据都是由后端来决定的,而后端工程师为了减少任务的反复,一般都会返回比前端所需更多的数据。比如:在某个页面前端只需要用户的idemail,如果是 RESTful 接口的话,为了接口能够更加通用,它可能还会携带别的信息

{
  "id": 1,
  "email": "[email protected]",
  "gender": "male",
  "role": "specialist"
}

哪怕genderrole在这个页面完全就用不上。而 GraphQL 的做法是,前端可以以查询字符串的方式告诉后端我们只需要某个资源的 XX,YY,ZZ 字段,麻烦给我们返回一下,其他的就别给我了。像下面的文章资源,我们通过查询字符串只获取了idtitle其他就不需要了

just-id-and-title.png

想要更多,则自己往里面加字段就行

id-title-and-excerpt.png

这么做的好处是,后端只需要返回前端所必要的数据,减少请求响应的数据量。

3. 强类型内省系统

一开始看 GraphQL 的时候,对那个内省 (Introspection) 系统是有些懵逼的,在编写 RESTful 接口的时候,我们一般都会通过 Swagger 之类的工具来给前端文档,前端可以根据这个文档来进行接口对接,并且可以在 Swagger 的页面上进行调试。

而在 GraphQL 里面似乎就不用这样子了,我个人感觉它有点像 Redux,后端定义好所有的动作,前端可以通过内省系统查询到这些动作,然后告诉后端系统,自己要执行哪个动作,后端系统就会调度对应的动作,完成前端交代的任务,并返回结果。

在 GraphQL 里面,动作主要分两种类型

  1. 查询用 (queryType)
  2. 修改用 (mutationType)

可以这样查询出他们两的名字跟描述

query-type-and-mutation-type.png

如果我们想进一步知道,后端所允许的查询动作,可以进一步去查找。从前面的结果可以知道,查询调度器的名字是Happy,看看它旗下有哪些“艺人”。

query-type.png

可以看到它支持的查询有Post, allPosts, Category, allCategories等等,还能查看到他们分别接收的参数,这些都是后台定义好的。只要运用熟练,这其实就相当于一份 API 文档,还是蛮方便的。

RESTful 的反击

我不确定大家是否喜欢上面这种模式,反正我本人不是很喜欢,对我来说,似乎有点过早优化了。前端许多社区似乎都有这样一个问题,Demo 看起来很无敌,实际应用的时候要你命

1. 你真的减少了往来的数据量?

按 GraphQL 这种说法,前端能够定义自己需要的数据,在一定程度上能够减少响应的数据量。然而,请求的数据量你有没有算进去?用 GraphQL 获取一个人的基本信息的时候大概是这个样子

// Post /graphql

query {
  user(id: 1) {
    name
    gender
    avatar {
      url
      signed_id
    }
    ....
  }
}

不过 RESTful 的话,直接/users/1就好了吧。要是我真的需要一个用户很多数据的话其实 RESTful 更方便。发送请求要容易许多,而 GraphQL 需要自己去定制前端所需要的每一个字段。用我前同事 Hugo 的说法是

这个地方有一个临界点,当 RESTful 一个接口返回很多前端用不着的字段时,才会节省数据量。

其实真的出现这种极端的情况,或许我们

  1. 直接针对这个接口做优化就好。
  2. 不然就另外提供一个/users/1/simple的接口,只返回精简的数据。
  3. 只针对这个接口的功能用 GraphQL 的模式来实现(没有洁癖的话)?

Why you shouldn’t use GraphQL 这篇文章提到了一个我挺认可的说法

GraphQL is an alternative to REST for developing APIs, not a replacement.

2. 减少了请求量,然而....

试想要是一个页面(比如落地页)需要调用十几二十个接口,你用 GraphQL 的话,确实是可以一个接口拿到所有的数据。举个例子,假设要填充首页需要有 10 个数组,那么如果用 RESTful 的话我们调 10 个接口,而 GraphQL 的话大概这样就能搞定

// Post /graphql

query {
  products1 {
    name
  }
  products2 {
    title
    sku
  }
  ..... // 还有8个类似的东西
}

不过兄弟,这还没完,如果这 10 个数组是需要分页的呢,那是不是有趣一些了?这种时候如果用 RESTful,各个组件会分别调动指定的接口,获取更多对应资源的数据。如果你用 GraphQL,那么你几乎不可能沿用上面的查询字符串,因为你预先并不知道哪个资源要获取更多数据。所以针对分页的情况你可能还要针对不同组件维护对应的查询字符串。大概是这样

// Post /graphql

query getProducts1($page: Int!){
  products1(page: $page) {
    name
  }
}
// query variables
{
  "page": 2 // 取决于第几页
}

以上模板还要写 9 个类似的,当然你也可以用工厂函数,然而这种情况用 GraphQL 似乎也没省多少事,就首次加载的时候 RESTful 要调 10 个接口,用 GraphQL 一个接口就搞定了,能节省掉一些请求。Emmmmmm,我针对这种情况用 RESTful 写一个专门的首页加载接口似乎也行?或者只针对首页加载的情况使用 GraphQL 技术,其他地方依旧用 RESTful?

3. 全都是 POST 请求,而且 Endpoint 都一个样...什么鬼

GraphQL 有个特点,就是请求链接都是一样的,而且全是 POST 请求,另外,正常来说所有请求不管成功失败都会返回 200。如果不点开请求详情,你根本不知道这个请求干了什么,下面这张图就比较生动了

all-post.png

链接都一样,只通过定义不同的动作,以及相关的参数来做不同的事情,或者说获取不同的数据。这样后端只需要定义好能用的那些动作,从此“高枕无忧”。反正我玩了好几天,越搞越纠结。RESTful 有个比较舒服的地方就在于,增删查改,弄得清清楚楚,利用了GETPOSTPUTDELETE这些语义化的动词,找问题要方便得多。并且对不同资源的操作会有不同的 endpoint(可理解为不同的 url),这样辨识度会更高。而 GraphQL 所有请求都是是统一入口的(假设是/graphql),只是利用不同的查询字符串来做不同的事情。

// 查询产品
query {
  product(id: 1) {
    name
    sku
  }
}
// 删除产品
mutation {
  removeProduct(id: 1) {
    product {
      name
      sku
    }
  }
}

product, removeProduct这些动作都后端定义好的,前端只需要告诉后端自己要干嘛,期望返回什么东西即可。只不过接口调用的结果,你只能自己查看响应内容才知道了。

error.png

可以看到,只要动作被执行了,哪怕是出错,接口都会返回 200,返回结果会有errors的字段,附上一堆错误消息。反正个人还是觉得 RESTful 这种能够通过不同状态码,还有请求类型来判断接口性质的做法比较“人性化”。

4. 不小心的改动容易出“人命”

其实写 GraphQL 接口的时候我已经忘记了自己在写 Ruby 了,哪怕 Ruby 本身是强类型语言,但是我们也不用凡事都去定义类型啊。而 GraphQL 感觉就是一套类型系统构建起来的东西,你得以声明的方式去定义很多的类。这当然也有它的安全性,起码我接收个参数,外层机制就能告诉前端,你传的类型不对。

不过也有难搞的地方,代码的方式是声明式的,前端能够获取什么数据都是后台来决定。比如,一个用户有name, id, gender这三个字段,如果后端限制你只能访问idname,你就不能访问gender。幻想一种情形,一开始后端给前端暴露了id, name, gender三个字段

module Types
  class PostType < Types::BaseObject
    description "Post Type"
    field :id, ID, null: false
    field :name, String, null: true
    field :gender, String, null: true # 后面会删掉
  end
end

前端也应用了这几个字段

query {
  user(id: 1) {
    id
    name
    gender
  }
}

某天一个不知情的后端把gender给删掉了,那么前端调用接口的时候就会直接报错,对于线上环境来说这还是挺致命的。毕竟它可能一个页面只发送了一次请求,所以请求中某个资源获取失败,会导致页面其他的资源也获取不了,前端页面有可能直接瘫痪。

breaking.png

官方倒是提过这个问题,它称之为Breaking Changes。它也提供了一些解决方案

  1. 把 Schema 结构 dump 出来,以便跟踪。
  2. 利用GraphQL::SchemaComparator在跑 CI 的时候检测新的提交是否有Breaking Changes

当然我相信适当的测试用例也能避免这种情况的发生,只是笔者当时就想:“这么折腾到底我还要不要用它?”

5. Rails 原生就比较亲近 RESTful

公司的后台用的 Rails,原生就比较亲近 RESTful 的做法,硬要上 GraphQL 也不是说不行。只是总感觉有那么点“不适应”---好吧,我承认是非常地不适应。

  1. 为什么我要不停地去定义类型?
  2. 为什么我们要把所有东西都放在一个 endpoint 去解决?
  3. 为什么我要让前端那么费劲去组装各种查询字符串?
  4. 为什么我要舍弃用得挺直观的 HTTP 请求动作(GET,PUT),还有各种有意义的状态码(500,404)?
  5. 为什么我要放弃 Rails 提供的在 Controller 里头十分方便的工具函数?
  6. .....

不知道社区的各位怎样,反正笔者是觉得越写越别扭了。似乎省下的时间都可以慢慢地优化以前的接口了。

尾声

经历过上面的考量,还有近期的实践之后,我觉得 GraphQL 我已经入门了,可以放弃了。于是乎跟前端小伙伴进行了一次灵魂的对话

我:“我感觉 GraphQL 有以上的 xxxx 问题,我们到底要不要用?” 前端小伙伴:“感觉增加了一堆的工作量,好处就.....” 我:“好处只增加了一丢丢?” 前端小伙伴:“不,目前都没感受到有什么好处。”

前几天我还在犹豫是否要在公司项目的新功能里面引入 GraphQL,现在基本是可以下定决心不用了。我并不否认,GraphQL 减少请求量,自定义数据返回这些好处都十分诱人,或许对于 Facebook,Github 这种量级的公司来说是个不错的选择,然而对我们这种初创公司来说真的经不起这般折腾,而且个人觉得有点过早优化了,省点时间跟朋友吃吃饭好了。

remove-graphql.png

反正我是放弃了,不知社区的朋友怎么看,你们开始用 GraphQL 了吗?

看前端项目规模了。个位数的前端团队 graphql 的收益应该不大。人多了,页面逻辑复杂的前端项目,与其写自动化测试,靠强类型来规避一些低级错误,成本更低

ian2hao 回复

那是有道理的。不过就算上 GraphQL 还是得写自动化测试。昨天也试了一下 https://graphql-ruby.org/testing/integration_tests.html 也不是说不行,就是感觉有点折腾。

hooopo 回复

说得我竟找不到词汇反驳。🐑

考虑到多端业务,GraphQL 应该有一席之地,安卓,ios,小程序,PC 等等,不同尺寸的屏幕显示不同字段的数据,PC 因为页面大,可能所有数据都显示,而客户端可能会省略一些数据。

还有一个场景,就是提供对外部的查询接口,除了这两个场景,其他用起来都比 RESTful 麻烦多了

我之前一个项目也上了,真是同样的感觉。在出现问题需要抓包的时候,不能直观的显示出问题所在。后来就觉得还是算了

其实可能只是你们的前端单纯的恶心 ActiveAdmin。

让他们自行选择用 AntDesign、ElementUI 或是 Vuetify 就解决问题了。 GraphQL 真的是没有必要。前后端都需要学习和适应,还要处理不熟悉的逻辑问题。 感觉比把前端培训会 Rails 的 model 和 restful 成本更高。

如果前端愿意 fullstack 的话自己随便整合一个 UI 框架,用 turbolinks 也是爽的不要不要的。

Graphql 对研发体验对提升是巨大的,graphql 没有版本的概念,最大程度将自由度给了前端,之前对前端等后端,后端改动了一个接口必须通知前端,测试回归等问题很大程度上没有了。这极大提升了迭代的速度,前后端也更加能够专注自己的专业领域。

jicheng1014 回复

😂 是,不过我们只是调研,还没到你那一步。

nine 回复

是有想过用 turbolinks 来重写后台了。要是前端没空的话。

robot_zhang 回复

低端用户阶段,目前倒是没体会到研发体验提升多少。😂

感觉不是 graphql 不好,而是相关基建还不够完善,很多地方没有自动化和最佳实践,导致体验上没发展完善的 restful 好。能力上 graphql 是比 restful 强大的。graphql 也没限制用户用 restful 的方式,一个一个资源的请求处理。只是额外多了指定返回内容。这部分指定的内容感觉可以根据用户的模型自动生成。而且还多了字段变更的强类型检查。

目前只用 restful api

说一个我自己在实践中使用 GraphQL 的感受

先说结论:如果是应用在完全公开的,仅提供查询功能的接口上,且前端同事明确的知道自己应当如何使用 GraphQL 的情况下,是完全 OK 的。

  1. 权限控制问题: 使用 GraphQL,权限控制(大概)只能做在资源侧,当面对各种资源嵌套调用的时候,权限控制的实现简直是个灾难。

  2. 查询复杂度问题: GraphQL 可以自定义查询条件,如果不对查询复杂度进行约束的话,你无法想象前端会写出一个多么复杂的 query,由此还可能引发内存泄漏问题,最后他们还会怪你性能做的不行。

lionzixuanyuan 回复

😂 看起来是血泪史。不过我们这边是增删查改,都有。后面还要做权限控制。所以还是决定不上先了。后面遇到那种大规模的查询请求再来补充几个接口好了。现在上 GraphQL 出活也慢。。。。。

lanzhiheng 回复

恒哥真的高产,每隔一两个星期都能分享一篇文章 👍 膜拜啊~

刚看到一篇 GraphQL 实践的文章,分享一下 https://mp.weixin.qq.com/s/BqsbopjTy10NtShJz8p8Gw

没有在生产环境用过 Graphql,但非常眼馋。理想情况页面数据组装,完全在服务端完成,通过 rest 风格接口返回当然是最简单的。

但随着业务逻辑复杂起来(比如一个维护 2-3 年的 erp 系统),一个管理页面页面可能涉及多个子系统的接口数据。graphql 由前端按需使用,好过手动请求 n 个接口正交数据,再手动拼装。

另外,graphql 的请求的深度限制和数量限制,都是基础操作,楼上对前端乱请求的担心多虑了

mark 一下,曾在项目中尝试用 graphsql,但是不是典型使用场景,没用起来

graphql 非常考验设计能力(把 schema 设计好,剩下的就交给前端堆积木了),同样考验前端的自觉能力(用到什么字段取什么字段,千万别一股脑的把没用的也取了,那对于后端就是噩梦了)

设计推荐帖(https://ruby-china.org/topics/40695),翻译的挺好

24 楼 已删除

19 年曾经用过 graphsql 在生产模式,不过说实话 复杂业务的话 graphsql 后端开发量也挺大的,因为首先你先要给出全集合,前端根据自己需要来调用你的子集,但是你先把全集写出来这个前提是必须的,这就很难受了。

kevinyu 回复

😂 感觉就比较难受。我却步了。

lanzhiheng 回复

使用一个技术栈的时候我一直觉得应该以你和你的团队是否擅长为首要因素,也有可能 graphsql 不差只是不合适你罢了。

个人感觉 graphql 更适合做查询,不太适合做修改类的接口。那么如果是复杂项目,架构可以用 CQRS 实现读写异构,即写接口用传统的 rest,读接口可以用 graphql 暴露,而 graphql 可以用 elastic search 作为只读数据源。elastic search 跟 RDS 做一个数据管道实时同步。这样查询的时候就不会出现复杂 query 导致数据库性能问题了,而且在数据同步的时候可以提前做数据合并,很适合 graphql 这种前端想查询什么就给什么的用例。(yy 一下 NoSQL 一张表打天下的情景)

这样是否一定程度上扬长避短了呢?

以上纯粹交流哈,感兴趣的可以一起探讨。 当然上面的方案有点跑题了,楼主勿怪~

kevinyu 回复

😂 是啊。我知道 GraphQL 优势多多,但我这小团队扛不住啊。要搞的话,RESTful 的业务就没人管了。

eedkevin 回复

😀 见识少还没搞过 CQRS,不过我感觉可以试试。听你这样说倒是还蛮吸引的,我且去研究研究。

lanzhiheng 回复

嗯哈,楼主可以调研下是否适合公司的业务场景。这个方案的好处是可以渐进式的重构,原有 rest 还可以继续跑。先重构一些典型复杂查询接口,成功的话再进行更深程度的重构。代价无非是多了个 es 集群和数据同步管道。

关于权限管理,感觉可以抽出来做一个独立的服务。graphql 里加一个 middleware 调权限管理服务再走正常业务逻辑,权限过不了就直接返回了。(其实一开始也可以先不独立服务,指回现有的权限模块就好,将来真的有必要了再抽出来作为独立的权限管理服务)

重构这种东西不宜步子迈得太大,容易整段垮掉。渐进式的重构方案可进可退,前期调研和试验后发现不合适放弃也不会造成太大损失,敏捷开发嘛

我觉得 graphql 和 rest 没必要对立起来,graphql 是对 rest 的适当补充,特别是对于复杂查询场景。

graphql 感觉还适合做 API 聚合器。例如有对外的公共接口,那么用 graphql 将内部一些 rest 接口包装一下,汇总成一个统一的公共接口入口还是蛮适用的。如果内部 rest 接口包含些敏感信息不宜暴露给外部的,还可以在 graphql 这层做过滤,完全不影响内部接口

无论 rest 也好,graph 也好,rpc 也好,都是接口风格。软件工程里没有银弹,好的架构需要具有一定程度的包容性,利于针对特定场景选择最适合的技术方案

33 楼 已删除

GraphQL 里授权(Authorization)怎么搞?总不能在每个 resolver 里都做一遍吧。

zhangkaizhao 回复

在 context function 里做 authorization 不难吧

zhangkaizhao 回复

哦,GraphQL Ruby 里对授权(Authorization)的支持设计得好像还不错:

几个实践

1每段ql可以有个唯一的key调用时直接用这个key即可对应的qlbody还可以编译缓存起来提高执行效率
2endpoint可以有很多个类似/gql/{占位符}然后执行占位符对应的ql
3授权可以统一做权限控制限流等可以配置在ql的元数据里比如deleteUser需要admin权限
4cors可跨域
5schema自省等可和配套设置联动起来比如文档中心对象定义fetch方法广场
6前端同学可以来写ql后端同学也可以
7可以直接在ql里访问db下的所有的表把表映射为一个对象
8前后端各自专注这个其他同学也提到了
9DTO对象组装数据裁剪和加工非常easy所见即所得
10最重要的一点生态工具太差了尤其说前端根据自己需要来调用你的子集】、【后端工作量也很多】,这种就是引擎的深度受害者限制了你的想象力我为啥这么说因为我手撕了一个新引擎这些都不是问题
11性能问题不不不引擎牛逼的话很多节点是可以并行执行的比你手写代码都快

先扯这么多。。。
darkbaby123 回复

你说的这些 REST 的缺点,JSON:API 似乎都补上了。

没用过 graphQL ... 出人命的那个,说下我的理解,不一定可用,当个参考 ...

如果想避免这个情况,应该让后端的逻辑是「应给尽给」的。

比如请求的需要 a b 两个部分,但是他无权请求 b,这时候返回应该是类似于这样的:

{
  a: { sth ... }
  b: {:error, "reason of why err"}
}

需要用一个 :error 的 atom 或者什么约定好的特殊内容的字符串做标记,后端只需要对应地告知前端哪里哪里你不能获取,但是能获取的部分应该正常返回,然后前端再决定怎么去处理这个不可获取的部分。

比如哪怕是把 ":error, you cannot get this!" 这个字符串直接展示给用户 —— 这也好过整个页面崩了。至少不会要命,而且甚至最终用户都可以参与到排查工作的流程中来 ... (他们兴许还会觉得这很 cooool(

总之,后端如果是设定成了(或者只能设定成)「只要某部分没法获取那么整个请求就失败啥都不给」的话,这其实是一种「越俎代庖」,它等同于是替前端规定了「如果你取到某部分是 :error 标记了那么整个获取到的数据就都该丢弃」这样一个错误处理逻辑。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号