5個用/不用GraphQL的理由
我在 ofollow,noindex">如何使用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" : "[email protected]", "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的侷限性和問題開始。
- 資料冗餘和請求冗餘 (overfetching & underfetching)
- 靈活而強型別的schema
- 介面校驗 (validation)
- 介面變動,維護與文件
- 開發效率
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的理由
- 遷移成本
- 犧牲Performance
- 缺乏動態型別
- 簡單問題複雜化
- 快取能解決很多問題
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而針對 Boost-Your-REST-API-with-HTTP-Caching.html" target="_blank" rel="nofollow,noindex">快取 作出的種種努力。
對於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,否則它可能不僅沒能提高開發者效率,反倒製造出更多的問題。