From REST to GraphQL(译)(上)

[译 Published 9 Oct 2015] https://jacobwgillespie.com/from-rest-to-graphql-b4e95e94c26b



声明:GraphQL仍然是一项新的技术,对它的实践还在不断的完善过程中。这篇文章描述了我根据目前学习到的知识点,搭建一个GraphQL后端的实现过程,希望能对你有所帮助。当然了,我已经对文章内部使用的例子PlayList(运动员列表)进行改述、简化和匿名了。

这篇文章假设你已经对GraphQL有了基础的了解。如果你不熟悉GrappQL,参考:https://code.fb.com/core-data/graphql-a-data-query-language/


REST

在PlayList这个例子中,我们基于Rails / REST原则开发API。最初设计这个API结构的灵感来自Github的V3 API,最终我们的API如下:

需要track信息?

GET /tracks/ID

获取一个playlist?

GET /playlists/ID

需要一个playlist的tracks?

GET /playlists/ID/tracks

这种命名方式直观简易,可以很容易的浏览。最初,为了方便浏览数据,我们甚至为所有的数据实现了URL可访问的API。移动端开发团队可以方便得通过文档描述去查询每一个端点的返回数据。

膨胀和延迟
然而,随着业务需求增加,API承载的数据越来越多。如下所示,是一个最简单的playlist返回内容:

{
  "created_at": "2015-08-30T00:50:25.000+00:00",
  "id": "e66637db-13f9-4056-abef-f731f8b1a3c7",
  "like_count": 3,
  "liked_count": 3,
  "name": "Excuse me while I kiss these frets",
  "owner": {
    "avatar_url": "https://secure.gravatar.com/avatar/4ede0ad35bb796ea8f78861acc4372ca?s=300",
    "bio": null,
    "id": "b06e671a-b169-45e6-a645-74c31abca910",
    "login": "playlistrock",
    "name": "Playlist Rock",
    "site_admin": false
  },
  "published": false,
  "saved_count": 3,
  "track_count": 50,
  "updated_at": "2015-09-30T06:11:49.000+00:00"
}

它包含了playlist的所有基础信息,几乎没有关联其他对象。但作为客户端来说,他希望在这个API中同时获取子资源。这样你就需要调用其他的端点,比如/playlist/ID/tracks。

越来越多的关联,使得playlist的返回结构越来越大。其实,由于我们使用了Rails和ActionView模版,为了给playlist列表增加更多数据,我们需要同步给_playlist.json.jbuilder这个模版增加更多数据。

还可能有这样的需求:当展示用户画像时,我们需要在用户playlist列表中增加前三个标签。移动端期待一次请求playlist搞定,也就是在playlist模版中增加标签;而不是先去请求/users/USERNAME/playlists,再去请求/playlists/ID/tags。

{
  "created_at": "2015-08-30T00:50:25.000+00:00",
  "genres": [],
  "id": "e66637db-13f9-4056-abef-f731f8b1a3c7",
  "like_count": 3,
  "liked_count": 3,
  "name": "Excuse me while I kiss these frets",
  "owner": {
    "avatar_url": "https://secure.gravatar.com/avatar/4ede0ad35bb796ea8f78861acc4372ca?s=300",
    "bio": null,
    "id": "b06e671a-b169-45e6-a645-74c31abca910",
    "login": "playlistrock",
    "name": "Playlist Rock",
    "site_admin": false
  },
  "published": false,
  "saved_count": 3,
  "tags": [
    {
      "name": "Jimi Hendrix"
    },
    {
      "name": "Jimmy Page"
    },
    {
      "name": "Eric Clapton"
    },
    {
      "name": "Slash"
    },
    {
      "name": "Stevie Ray Vaughan"
    }
  ],
  "track_count": 50,
  "updated_at": "2015-09-30T06:11:49.000+00:00"
}

/playlists/ID 的返回结果还有可能如下:

{
  "collaborators": [],
  "created_at": "2015-08-30T00:50:25.000+00:00",
  "genres": [],
  "id": "e66637db-13f9-4056-abef-f731f8b1a3c7",
  "like_count": 3,
  "liked": true,
  "liked_count": 3,
  "name": "Excuse me while I kiss these frets",
  "owner": {
    "avatar_url": "https://secure.gravatar.com/avatar/4ede0ad35bb796ea8f78861acc4372ca?s=300",
    "bio": null,
    "id": "b06e671a-b169-45e6-a645-74c31abca910",
    "login": "playlistrock",
    "name": "Playlist Rock",
    "site_admin": false
  },
  "published": false,
  "saved": true,
  "saved_count": 3,
  "tags": [
    {
      "name": "Jimi Hendrix"
    },
    {
      "name": "Jimmy Page"
    },
    {
      "name": "Eric Clapton"
    },
    {
      "name": "Slash"
    },
    {
      "name": "Stevie Ray Vaughan"
    }
  ],
  "track_count": 50,
  "tracks": [
    {
      "album": {
        "id": "8d8223c6-284c-4aac-92bd-b31debca3237",
        "title": "Toys In The Attic"
      },
      "artists": [
        {
          "id": "6c29ff27-ad20-4448-9961-f6617e393539",
          "name": "Aerosmith"
        }
      ],
      "explicit": false,
      "have_liked": false,
      "id": "a1f9f37a-2a15-407d-82f8-e742ab5e3b81",
      "title": "Walk This Way"
    },
    {
      "album": {
        "id": "21a9f63b-a38f-40f1-aaf1-8b7ed3ad1a92",
        "title": "Audioslave"
      },
      "artists": [
        {
          "id": "7d600588-d073-41e9-a4f7-434501b16c45",
          "name": "Audioslave"
        }
      ],
      "explicit": false,
      "have_liked": false,
      "id": "4cc1fc43-61e8-49a7-be42-9d7ad35c1284",
      "title": "Like A Stone"
    }
  ],
  "updated_at": "2015-09-30T06:11:49.000+00:00"
}

这里我们嵌入了tracks,甚至还关联了tracks的子集。返回足够多的数据,就能够覆盖到所有可能访问playlist的地方。同时,每一处请求都会返回这些数据。

本可以通过访问更多端点的方式实现一样的功能,如:/playlists/ID/forProfile,/playlists/ID/forNotifications等。所以增大返回数据结构的设计决定其实也是有意为之。

这被认为是一种简单的提供数据的方式。例如,为了增加track字段,你只需在_tracks.json.jbuilder模版中增加相应的字段。但随之view的增加,效率问题开始变得显著起来。两个明显的原因很如下:

首先,响应体比较大,有时移动端APP都要有非常大的工作量去解析、反序列化和存储Json。响应时间变得更长,缓存变得更大,每一次对局部的小改动都会对整个APP造成影响。

其二,当一次请求有越来越多的数据需要获取(尤其是有关联关系)时,查询效率将会受到影响。在没有缓存的情况下,对playlist的单次请求,可能需要多达170次数据库查询,才能拉取所有的相关信息。

生产中,我们大量使用了“俄罗斯套娃”式缓存模式,所以对于一个完全缓存的playlist,只需要一次数据库查询。然而,第一次加载时,为了构建完整的响应,仍然需要执行那170次查询(这时俄罗斯套娃式缓存或者共享子资源没多大帮助)。

把我们推向崩溃边缘的是have_liked字段(如上图)。这是一个bool值,它决定了当前认证用户是否喜欢某一个track。产品需求规定了在playlist详情页这个字段是必需的,从而playlist响应中的每一个track item都要添加这个字段。

这打破了“俄罗斯套娃”缓存。

_track.json.jbuilder就成为了一个含有tracks静态信息的可缓存部分和需要调用 current_user.have_liked(track)方法的不可缓存部分的局部组合体。随后,_playlist.json.jbuilder或者其他(局部)关联track信息的view,都要为了包含可缓存和不可缓存部分做类似转化。

更糟糕的是,请求一个包括50个tracks的playlist时,需要执行50次have_liked()方法(N+1 query bug)。

我们有一些可能的解决方案,如为不同的节点拆分子资源view文件,自定义可缓存的查询以减少多余的查询次数等等。不过,我们想拥有另一种全新的解决方案,它不仅可以处理目前遇到的问题,还给了我们更大的自由度。


GraphQL

打开GrapgQL的世界。使用GrapgQL增强了我们的后端能力,我们可以为每一次客户端请求提供精确的无冗余的数据;通过优化数据库或缓存层,我们能够以相当高效的方式做任何事情。

在获取详细细节前,先阐述下我在学习GraphQL过程中遇到的几个常见问题/错误概念:

常见问题/错误概念

GraphQL听起来像图表。我的数据需要是“图表化”的吗或者我需要一个“图表”数据库吗?它只应用于关系数据库吗?

不,你不需要图表数据库,它可以应用于目前你使用的任何数据库。

也许内心深处,你认为几乎任何的“关系型”数据库有着如下的“图表”结构:

user --- OWNS --- playlist
  |                   |
LIKES              CONTAINS
  |                   |
  v                   |
track <---------------┘

实际上,GraphQL描述或者获取数据数据的方式更像一个树形结构。

user
┖-OWNS-> playlist
         ┖-CONTAINS-> track
                      ┖-LIKED_BY-> users

你可以使用图表数据库,关系数据库,甚至是内存中的数组,存储的键值对等等。在Playlist中,我们使用Neo4j作为“基础”数据库管理了一个完全的图表模型;我们使用Redis作为缓存层,它使用了大量不同的数据结构,包括哈希值、键值对、集合等。在Neo中,Redis实际上存储的是通过 ID and ZSETs 来关联的键值对数据。嗯,差不多镜像了Facebook的TAO模式

通过充分利用有着90%机会命中in-memory键值对存储的Cypher查询,我们在Neo领域有着最权威的数据源。

GraphQL听起来像是“查询语言”,这是不是意味着我正在暴露给客户端查询数据库的能力。还是蛮危险的,如果遇到了恶意客户端怎么办?

不,相比于REST API,你并没有更多的暴露给客户端查询数据库的能力。好吧,也许有一点点。

GraphQL类似于一个数据模拟语言(DSL),它建立在你的后台数据获取逻辑之上。它不会直连数据库。实际上,通过GraphQL暴露的schema不可能是数据库的精确镜像。它只是提供了一个方式去描述一个请求要组织的数据,它仍然依赖后端去完成这个请求。

我们应该担心的是GraphQL支持嵌套查询。一个恶意的客户端可能会在任意时间大量的请求一组带有递归嵌套关系的数据(比如 user.followers.followers...)。这会对后端性能造成潜在的打击。最后的部分,我们将会讲到如何规避这种风险。

所以,未认证时,通过GraphQL并不会访问到我的数据库?

是的。验证完全在GraphQL之外处理,就像你在使用REST时一样,由后端负责以安全的方式进行数据的获取/验证。

比如我们自己的GraphQL后端的处理就是:将请求头传递给后端,由后端验证这个请求,最后将验证上下文传递给GraphQL数据解析器。

在Playlist这里例子里,我们甚至把GraphQL后端改造成了一个“运输通道”。我们可以像平常一样,通过HTTP获取数据,也可以通过非HTTP连接协议请求数据。比较酷的是,通过MQTT我们以类流媒体的方式实现了对即时数据的更新。同样的,我们也可以为GraphQL请求植入一些验证信息,如验证tokens或者用户名/密码对等,看是否可以辅助验证。目前,我们还没有充分探索这些方法。

安全性怎么样?

在说一次,这完全取决于你的后端实现,并不是GraphQL要考虑的内容。后面我们将会看到认证之后的解析器实现(获取和返回数据的函数)。在这之前,当一个客户端试图去访问没有权限的数据时,我们大概有两种方式去处理。

第一,请求字段返回空。假设在请求一组特定的数据时,拒绝返回没什么影响时,这是一个很好的选择。

举个🌰,后端只提供用户自己的邮箱地址时,我们请求用户的email字段。如果我请求自己的用户信息,当然了,包括email字段,将会得到邮箱地址;如果请求其他用户信息,email字段将返回空。我只需保证在该字段返回空时,程序依然运行正常就可以。

第二,返回一个真实的错误。这是一个不错的方案。因为客户端请求数据时,希望知道为什么后端没提供想要的数据,这样它就可以通过返回信息采取一些措施。

例如,我们尝试去请求一个需要认证信息的对象时,并没有提供认证信息。

“404系列错误”(404s)通常被当做空返回。约定俗成的(如 Github's API),未认证对象有时也会返回空(如请求一个已经被封号的用户简介)。空模拟了404,并且不会泄露隐藏用户存在的事实。

Github上仓库太多了!哪一个是真正的GraphQL?

facebook/graphql is the specification for the GraphQL language and its implementation - it is not tied to any specific language / backend. It's great to read to fully understand the language, especially if you're into those things or learn best by digging into concepts and theories.

graphql/graphql-js is a reference implementation of that specification provided by Facebook, written in JS/Node. This is the place to start if you'd like to use GraphQL with a Node-based backend or just want to play around. To the best of my knowledge, this is the most complete implementation of the specification, being more or less the official reference implementation. Read the README.

graphql/express-graphql is a middleware for Express.js to easily create a GraphQL server with Express. I'd highly recommend reading the entire source code as it's not terribly long, is quite easy to understand, and lends itself to explaining how to use graphql-js, even if you don't end up using express-graphql directly.

graphql/graphql-relay-js is a set of helpers to implement Relay-compatible IDs and "connections" (one to many associations, or array fields) - it is not required to use GraphQL, however we have found that being Relay-compatible has benefited us even though we're not using Relay, with ID handling, pagination, etc. For more information on the Relay GraphQL specification, see the Relay docs.

graphql/graphiql is a web-based IDE for GraphQL. This thing is freaking awesome. GraphQL provides schema introspection, and GraphiQL provides autocomplete and syntax validation using those introspection capabilities. You can download this project directly, embed it in your app, or my favorite, download it as a standalone app in an Electon-based wrapper at skevy/graphiql-app.

facebook/dataloader is a utility module that has revolutionized data fetching in our Playlist backend. Its foundation is extremely simple - it collects the arguments of calls to load() while in the current frame of execution (an event loop tick) and then uses your custom provided logic to batch-fetch data based on the collected arguments. More on how we use DataLoader below.

graphql/swapi-graphql is an example project exposing the existing SWAPI as a GraphQL server. It utilizes graphql-js, express-graphql, GraphiQL, and DataLoader.

chentsulin/awesome-graphql is an awesome collection of links to GraphQL resources, projects, posts, and more. Check it out!

Relay是什么?我也需要使用Relay吗?

Relay 是一个连接GraphQL和React的智能框架。当然了,你不必依赖Relay去使用GraphQL,但假使你正在使用React开发app,拉取一下吧,它极可能帮助到你的。

为了支持Relay,你需要在设计GraphQL查询时实现几个特别的约定;尽管我们也不需要Relay,但在Playlist的开发中,我们依然兼容了Relay约定。这样,无论是通过ID获取运动员信息或者展示运动员信息抑或为一些关联关系分页,我们都提供了一致性的API。更详细的介绍可以查看Relay官方文档。

只能在React中使用GraphQL吗?

不。在任何你之前使用HTTP/REST的地方你都可以使用GraphQL。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容