5個用/不用GraphQL的理由

我在如何使用Gatsby建立博客 / How to build a blog with Gatsby這篇文章中提過GraphQL在Gatsby中的應用。總的來講,它是一個新潮的技術,在適宜的使用場景威力無窮。這里我們來討論一下用/不用GraphQL的理由吧。

簡單介紹GraphQL

GrahQL

GraphQL是Facebook2015年開源的數據查詢規范。現今的絕大多數Web Service都是RESTful的,也就是說,client和server的主要溝通模式還是靠client根據自己的需要向server的若干個endpoint (url)發起請求。由于功能的日漸豐富,對Web Application的要求變得復雜,REST的一些問題逐漸暴露,人們開始思考如何應對這些問題。GraphQL便是具有代表性的一種。GraphQL這個名字,Graph + Query Language,就表明了它的設計初衷是想要用類似圖的方式表示數據:即不像在REST中,數據被各個API endpoint所分割,而是有關聯和層次結構的被組織在一起。

比方說,假設這么一個提供user信息的REST API: <server>/users/<id>,和提供用戶的關注者的API:<server>/users/<id>/followers,以及該用戶關注對象的API: <server>/users/<id>/followed-users。傳統的REST會需要3次API call才能請求出這三份信息(假設<server>/users/<id> 沒有包含followers and followed-users信息,which will be a definite redundancy if it does):
1 GET <server>/users/<id>

{
 "user": {
    "id" : "u3k2k3k178",
    "name" : "graph_ql_activist",
    "email" : "graph_ql@activist.com",
    "avatar" : "img-url"
  }
}

2 GET <server>/users/<id>/followed-users
3 GET <server>/users/<id>/followers

然而如果使用GraphQL,一次API請求即可獲取所有信息并且只選取需要的信息(比如關于用戶只需要name不要email, followers只要最前面的5個name,followed-users只要頭像等等):

query {
  user (id : "u3k2k3k178") {
    name
    followers (first: 5) {
      name
    }
    followed-users {
      avatar
    }
  }
}

我們會得到一個完全按照query定制的,不多不少的返回結果(一般是一個json對象)。

5個使用GraphQL的理由

使用GraphQL的理由, 必然是從討論RESTful Service的局限性和問題開始。

  1. 數據冗余和請求冗余 (overfetching & underfetching)
  2. 靈活而強類型的schema
  3. 接口校驗 (validation)
  4. 接口變動,維護與文檔
  5. 開發效率

1 數據冗余和請求冗余 (overfetching & underfetching)

根據users API的例子,我們可以想見,GET用戶信息的REST call,我們就算只是想要一個用戶的一兩條信息(比如name & avatar),通過該API,我們也會得到他的整個信息。所謂的overfetching就是指的這種情況——請求包含當前不需要的信息。這種浪費會一定程度地整體影響performance,畢竟更多的信息會占用帶寬和占用資源來處理。

同樣從上面的例子我們可以看出來,在許多情況下,如果我們使用RESTful Application,我們常常會需要為聯系緊密并總量不大的信息,對server進行多次請求,call復數個API。

舉一個例子,獲取ID為"abc1"和"abc2"的兩個用戶的信息,我們可能都需要兩個API call,一百個用戶就是一百個GET call,這是不是很莫名其妙呢?這種情況其實就是underfetching——API的response沒有合理的包含足夠信息。

然而在GraphQL,我們只需要非常簡單地改變schema的處理方式,就可以用一個GET call解決:

query {
  user (ids : ["ab1", "abc2", ...])
}

我們新打開一個網頁,如果是RESTful Application,可能請求數據就會馬上有成百上千的HTTP Request,然而GraphQL的Application則可能只需要一兩個,這相當于把復雜性和heavy lifting交給了server端和cache層,而不是資源有限,并且speed-sensitive的client端。

2 靈活而強類型的schema

GraphQL是強類型的。也就是說,我們在定義schema時,類似于使用SQL,是顯式地為每一個域定義類型的,比如說:

type User {
  id: ID!
  name: String!
  joinedAt: DateTime!
  profileViews: Int! @default(value: 0)
}

type Query {
  user(id: ID!): User
}

GraphQL的schema的寫作語言,其實還有一個專門的名稱——Schema Definition Language (SDL)。

這件事情的一大好處是,在編譯或者說build這個Application時,我們就可以檢查并應對很多mis-typed的問題,而不需要等到runtime。同時,這樣的寫作方式,也為開發者提供了巨大的便利。比如說使用YAML來定義API時,編寫本身就是十分麻煩的——可能沒有理想的auto-complete,語法或者語義有錯無法及時發現,文檔也需要自己小心翼翼地編寫。就算有許多工具(比如Swagger)幫助,這仍然是一個很令人頭疼的問題。

3 接口校驗 (validation)

顯而易見,由于強類型的使用,我們對收到的數據進行檢驗的操作變得更為容易和嚴格,自動化的簡便度和有效性也大大提高。對query本身的結構的校驗也相當于是在schema完成后就自動得到了,所以我們甚至不需要再引入任何別的工具或者依賴,就可以很方便地解決所有的validation。

4 接口變動,維護與文檔

RESTful Application里面,一旦要改動API,不管是增刪值域,改變值域范圍,還是增減API數量,改變API url,都很容易變成傷筋動骨的行為。

如果說改動API url(比如/posts --> /articles),我們思考一下那些地方可能要改動呢?首先client端的代碼定然要改變request的API endpoint;中間的caching service可能也需要改要訪問的endpoint;如果有load balancer, reverse proxy,那也可能需要變動;server端自己當然也是需要做相應改變的,這根據application自己的編寫情況而定。

相比之下,GraphQL就輕松多了。GraphQL的Service,API endpoint很可能就只有一個,根本不太會有改動URL path的情況。至始至終,數據的請求方都只需要說明自己需要什么內容,而不需要關心后端的任何表述和實現。數據提供方,比如server,只要提供的數據是請求方的母集,不論它們各自怎么變,都不需要因為對方牽一發而動全身。

在現有工具下,REST API的文檔沒有到過分難以編寫和維護的程度,不過跟可以完全auto-generate并且可讀性可以很好地保障的GraphQL比起來,還是略顯遜色——畢竟GraphQL甚至不需要我們費力地引入多少其他的工具。

再一點,我們都知道REST API有一個versioning: V1, V2, etc.這件事非常的雞肋而且非常麻煩,有時候還要考慮backward compatibility。GraphQL從本質上不存在這一點,大大減少了冗余。增加數據的fields和types甚至不需要數據請求方做任何改動,只需要按需添加相應queries即可。

另外,有了GraphQL的queries,我們可以非常精準地進行數據分析(Analytics)。比如說具體哪些queries下的fields / objects在哪些情況下是被請求的最多/最頻繁的——而不像RESTful Application中,如果不進行復雜的Analytics,我們只能知道每個API被請求的情況,而不是具體到它們內含的數據。

5 開發效率

相信上面說的這些點已經充分能夠說明GraphQL對于開發效率能夠得到怎樣的提升了。

再補充幾點。

GraphQL有一個非常好的ecosystem。由于它方便開發者上手和使用-->大家爭相為它提供各種工具和支持-->GraphQL變得更好用-->社區文化和支持更盛-->... 如同其他好的開源項目一樣,GraphQL有著一個非常好的循環正向反饋。

對于一套REST API,哪怕只是其使用者(consumer),新接觸的開發者需要一定時間去熟悉它的大致邏輯,要求乃至實現。然而GraphQL使用者甚至不需要去看類似API文檔的東西,因為我們可以直接通過query查詢query里面所有層級的type的所有域和它們各自的type,這不得不說很方便:

{
  __schema {
    types {
      name
    }
  }
}

==> 我們可以看到query所涉及的所有內容的類型:

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "Episode"
        },
        {
          "name": "Character"
        },
        {
          "name": "ID"
        },
        {
          "name": "String"
        },
        {
          "name": "Int"
        },
        {
          "name": "FriendsConnection"
        },
        {
          "name": "FriendsEdge"
        },
        {
          "name": "PageInfo"
        }
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__EnumValue"
        }
        }
      ]
    }
  }
}

對于GraphQL,我還有個非常個人的理由偏愛它:對于API的測試,相比于比較傳統的Postman或者自己寫腳本進行最基本的http call(或者curl),我更喜歡使用insomnia這個更為優雅的工具。而在此之上,它還非常好地支持了GraphQL,這讓我的開發和測試體驗變得更好了。(Postman至今還不支持GraphQL,雖然本質上我們可以用它make GraphQL query call)

5個不用GraphQL的理由

  1. 遷移成本
  2. 犧牲Performance
  3. 缺乏動態類型
  4. 簡單問題復雜化
  5. 緩存能解決很多問題

1 使用與遷移成本

現有的RESTful Application如果要改造成GraphQL Application?

hmmm...

我們需要三思。首先我就不說RESTful本來從end to end都有成熟高效解決方案這樣的廢話了。遷移的主要問題在于,它從根本上改變了我們組織并暴露數據的方式,也就是說對于application本身,從數據層到業務邏輯層,可能有極其巨大的影響。所以它非常不適合現有的復雜系統“先破后立”。一個跑著SpringMVC的龐大Web Application如果要改成時髦的GraphQL應用?這個成本和破壞性難以預計。

并且,盡管我們說GraphQL有著很好的社區支持,但本質上使用GraphQL,就等于要使用React與NodeJS。所以如果并不是正在使用或者計劃使用React和Node,GraphQL是不適合的。

2 犧牲Performance

Performance這件事是無數人所抱怨的。如同我們前面所說的,GraphQL的解決方案,相當于把復雜性和heavy lifting從用戶的眼前,移到了后端——很多時候,就是數據庫。

要討論這一點,我們首先要提的是,為了支持GraphQL queries對于數據的查詢,開發者需要編寫resolvers。

比如說這樣一個schema:

type Query {
  human(id: ID!): Human
}

type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  name: String
}

對于human,我們就需要一個最基礎的resolver:

Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

當然這還沒完,對不同的請求類型,我們要寫不同的resolver——不僅原來REST API的CRUD我們都要照顧到,可能還要根據業務需求寫更多的resolver。

這件事情造成的影響,除了開發者要寫大量boilerplate code以外,還可能導致查詢性能低下。一個RESTful Application,由于每個API的確定性,我們可以針對每一個API的邏輯,非常好的優化它們的性能,所以就算存在一定程度的overfetching/underfetching,前后端的性能都可以保持在能夠接受的范圍內。然而想要更普適性一些的GraphQL,則可能會因為一個層級結構復雜而且許多域都有很大數據量的query跑許多個resolvers,使得數據庫的查詢性能成為了瓶頸。

3 缺乏動態類型

強類型的schema固然很省力,但是如果我們有時候想要一些自由(flexibility)呢?

比方說,有時候請求數據時,請求方并不打算定義好需要的所有層級結構和類型與域。比方說,我們想要單純地打印一些數據,或者獲取一個user的一部分fields直接使用,剩下部分保存起來之后可能使用可能不使用,但并不確定也不關心剩下的部分具體有那些fields——多余的部分可能作為additional info,有些域如果有則使用,沒有則跳過。

這只是一個例子,但是并不是一個鉆牛角尖的例子——因為有時候我們所要的objects的properties本來就可能是dynamic的,我們甚至可能會通過它的properties/fields來判定它是一個怎樣的object。

我們要怎么處理這種問題呢?一種有些荒誕現實主義的做法是,往Type里加一個JSON string field,用來提供其相關的所有信息,這樣就可以應對這種情況了。但是這是不是一個合理的做法呢?

4 簡單問題復雜化

最顯著的例子,就是error handling。REST API的情況下,我們不需要解析Response的內容,只需要看HTTP status code和message,就能知道請求是否成功,大概問題是什么,處理錯誤的程序也十分容易編寫。

然而GraphQL的情景下,hmmm...

只要Service本身還在正常運行,我們就會得到200的HTTP status,然后需要專門檢查response的內容才知道是否有error:

 {
      "errors": [
        {
          "message": "Field \"name\" must not have a selection since type \"String\" has no subfields.",
          "locations": [
            {
              "line": 31,
              "column": 101
            }
          ]
        }
      ]
    }

Another layer of complexity.

同時,簡單的Application,使用GraphQL其實是非常麻煩的——比如前面提到的resolvers,需要大量的boilerplate code。另外,還有各種各樣的Types, Queries, Mutators, High-order components需要寫。相比之下,反倒是REST API更好編寫和維護。

5 緩存能解決很多問題

編寫過HTTP相關程序之后應該都知道,HTTP本身就是涵蓋caching的,更不要提人們為了提高RESTful Application的performance而針對緩存作出的種種努力。

對于overfetching和請求次數冗余的問題,假設我們的整個application做了足夠合理的設計,并且由于REST API的固定和單純性,緩存已經能非常好地減少大量的traffic。

然而如果選擇使用GraphQL,我們就沒有了那么直白的caching解決方案。首先,只有一個API endpoint的情況下,每個query都可能不同,我們不可能非常輕松地對request分門別類做caching。當然并不是說真的沒有現成的工具,比如說Appollo client就提供了InMemoryCache并且,不論有多少queries,總是有hot queries和cold ones,那么pattern總是有的。針對一些特定的query我們還可以定向地緩存,比如說PersistGraphQL便是這樣一個工具。然而這樣做其實又是相當于從queries中提煉出類似于原來的REST API的部分了,并且又增加了一層complexity,不管是對于開發還是對于performance,這都可能有不容忽視的影響。

總結

GraphQL最大的優勢,就是它能夠大大提高開發者的效率,而且最大化地簡化了前端的數據層的復雜性,并且使得前后端對數據的組織觀點一致。只是使用時,需要考察scale, performance, tech stack, migration等等方面的要求,做合理的trade-off,否則它可能不僅沒能提高開發者效率,反倒制造出更多的問題。

References

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容