GraphQL 你好你好

Yang Hsing Lin
Jun 7 · 11 min read

(這篇文章是由 YanghsingIan 共同完成)

大約在去年底,Codementor 正式在 production 的產品上使用 GraphQL。但其實在幾年前 GraphQL 剛問世不久後,我們評估的結果是暫時不考慮的。在這篇文章裡面,我們想和大家分享這中間的轉折。也就是關於在選擇它的時候,我們面對的取捨和導入的方法。

  • 取捨:在什麼情境下使用這樣子的技術對我們來說會是一個”划算”的選擇呢?
  • 導入的方法:如果要用的話,要怎麼和現有的技術優雅地整合呢?

下面會一一介紹:

  • 我們認知的 GraphQL 是什麼:一些最基本的介紹,目的是為了給這篇文章足夠的情境。
  • 為什麼一開始沒有使用呢?
  • 那現在是為了什麼來用呢?
  • Back-end 導入的方法和原因
  • Front-end 導入的方法和原因

根據 GraphQL 的官網:

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.

它的優勢在於:

gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

也就是說,想像本來在 REST 的世界裡,front-end 和 back-end 的合作方式是雙方共同討論一組 API 的格式,然後根據這樣的格式各自開發。其中”格式”,包括有哪些 resources, 支援哪些 method,到每個 API 下面有什麼 keys (可能是巢狀的結構等等);現在變成由 back-end 提供一組 “schema”,其中定義了各個 typefield,還有 “type 們之間的關係”。以這個定義為雙方的介面,front-end 可以自由地(當然還是在某些設定好的規範下)取用任何在介面裡的 type 和 fields。

用萬年老梗舉例來說,假設我們有 “user” 會產出 “posts”。在 REST API 下我們可能會定義這樣的 API:

而在 GraphQL 下面的作法會是定義出 “post” 和 “user” 這兩種 type, fields,和它們之間的關係(“post” 有一個 “user” field, “user” 下面有一個 “posts” field)。而在這個定義之下,client 可以決定使用這個 API 的時候要拿哪些 fields。

大約在兩三年前,當我們做完最初步的 survey 之後,當下是決定暫時先不導入這樣的技術。下面是我們考量的順序:

  1. 前後端的介面

首先我們先從前後端的介面開始評估。會從這邊開始是因為,我們認為如果想要加速開發的速度的話,加速”人和人之間合作的介面”是一個重要點。而以我們當時的狀況來說,前後端的溝通佔了工程師之間的溝通的很大部份。也就是說,在導入一個新的技術的時候,如果會因此讓人和人之間的合作介面變得複雜或混亂的話,那基本上我們是不太考慮的。

我們認為 GraphQL 一個很主要的優勢在於,front-end 在使用 API 的時候會很有彈性。也就是說,想像今天因為任何的原因,front-end 想要在本來的 API 多拿一個 field, 又或者是想要調整 API 回傳的格式,像是巢狀的結構等,這時候如果用 GraphQL 就會非常的方便,因為這些 fields 和格式都是 front-end 可以自己決定的,並不是定義在前後端的介面裡面。最常見的就是有各種不同的 front-end (像是 web, ios, android) team 都要使用 API,這時候這樣的介面定義就可以讓這方面開發的成本減少很多。

但當時我們認為(現在也還是這麼認為)在短中期內,我們不會有各式各樣有不同需求的 front-end teams 出現,多半只會有一個(web)至多兩個(加上手機 app),所以這樣的優勢對我們來說的幫助並不大。再者,以當時的經驗來說,我們很少 front-end 會純粹想要改 API 的格式。通常 front-end 要改動 API 大多是整個 feature modeling 的方式改變,所以 back-end 不管如何也要做相對應的調整。

所以在前後端的介面來說,GraphQL 對於當時的我們是處於一種”導入的話也沒有壞處,但也沒什麼特別好處” 的狀態。

2. 後端的開發

在考量完前後端的介面之後,接著我們才開始各自看前後端的開發。

就像官網裡介紹的,GraphQL 本身是一個 query 的介面,它並沒有和任何 database 綁定在一起。也就是說,要使用 GraphQL 的話,在後端是需要在 application layer 實作它(當然是需要靠 library XD),讓這整個 query language 可以運作。

2.a Database 的考量

Codementor 目前主要的 database 是使用 postgresql。在某些商業邏輯比較複雜的狀況下,我們可能要寫相對複雜的 sql 去把 data query 出來。如果要把這些複雜的 query 寫進 GraphQL 的 loader 裡面的話,我們評估技術上雖然可以做得到,但也可以預見在某些情形下會有很複雜的 loader。

主要的原因是因為本來這些 query 的邏輯是被分別寫在每一個 REST API 裡面的,但在 GraphQL 下,由於 query 的方式是由 client 直接決定,這些複雜的 query 要被轉換成 general 的 loader。解決的方式有考慮在現有的 table 上加上 view/materialized view 來簡化 loader 的複雜度和增加效能,但考量到整體帶來的好處不多,於是就到此為止沒有深入下去了。

2.b writing business logic

除了讀取的 API 之外,現有的 API 也還有很大量的寫入 API。而在我們的商業邏輯下,大部份的寫入都不是只是單純的修改資料而已;多半的寫入 API 會伴隨著各種邏輯的驗証還有 side effect,好比說寄信、打 notification 等等。

目前我們是用 Service Object 來做的。換句話說,多半的 updating service 很有可能會牽涉到一個以上的 resource update。在這樣的狀況下,我們覺得如果硬要用 GraphQL 來表現的話,反而會讓整個 API 的理解門檻更高。於是從這點來看的話,是暫時不考慮 GraphQL。

為了決定 REST API 的格式的討論(大誤)

Codementor 作為一個 market place,除了面對 user 端的產品外,面對內部的後台也是我們開發的重點之一。而這些後台因為作為 supply 和 demand 中間的橋樑,它們的邏輯和開發速度甚至比面對 user 的還要複雜和快速。如何在這樣快速又複雜的演進下還可以優雅的進行開發,一直是我們努力的方向。

隨著我們產品的演進,我們發現在後台,我們開始要用很多不同的角度去檢視同樣的 data。所謂 “不同的角度” 常常是 sales 或 operation 的夥伴們,需要方便地看到各種 user, mentor 狀態的組合。而這些 “角度”,有的時候只是一些嘗試的起點。也就是說,常常會有新的組合的需求出現。每當這類需求出現的時候,back-end 就要開始做新的 API 把本來既有的 data 重新打包,而 front-end 則是要開始傷腦筋該怎麼 model 這種 “看起來是新的其實是舊的 resources”。

這對我們來說是一個新的需求情境,也正好打中 GraphQL 的強項:可以讓 front-end 任意組合讀取的 data。於是我們開始做一個 prototype 試著導入 GraphQL 看看。

我們定義的目標是希望:在 GraphQL 加入我們的 stack 之後,如果再有這類型組合 data 的需求,只需要前端修改呈現的方式和用新的格式接 data,後端是幾乎可以不用改動的。

接著,我們開始思考要怎麼分別在 back-end 和 front-end 分別就現有的架構去把 GraphQL 加進來。下面會分別介紹導入的做法。但在這之前,我們的需求是:

  • 基本上只有 read API 會是 GraphQL,寫入的 API 還是 REST
  • 因為是後台的使用,所以 access control 相對單純。要嘛就是全部的 resource 都可以 access,要嘛就是都不行。
  • 目前的 db 是用 postgres,有時候因為實作的考量,table 並不一定會和商業邏輯中的 entity 有一對一的對應(例如說會有 join table 等)。但我們不希望因為導入的 GraphQL,就把這些實作的細節曝露給 front-end。
  • db 的 performance 要照顧,如果 GraphQL 一來 n + 1 query 就狂噴那是不行地。

目前我們後端是 Rails + Postgres。在 survey 後端導入的方法時,我們有考慮過下面幾個選項:

  1. 利用像是 postgraphile 的工具,在 postgres 這層直接放出 graphql 的介面。這樣的好處是很全面,就是一裝上去之後,所有的 resource 幾乎都直接完成。基本的 N+1 query 它也都有 cover 到。但壞處則是會把 table schema 和前端的邏輯 couple 起來。也就是說,沒有一個中間的 application layer作為中介,在 schema 改動的時候前端可能會不預期的壞掉。
  2. 在 Rails 這層,用 graphql-ruby 搭配 graphql-batch 來做。在 Rails 之上建立一個 graphql 的 endpoint,用 ruby 定義各種 resource 和 loader。這樣的成本是我們要自己做比較多 loader(尤其是當 query 比較複雜的時候),但好處是 GraphQL 的切入點是在 application layer 而不是 db layer,所以我們可以對商業邏輯封裝的空間比較大,access control 也可以比較靈活。

後來在一番討論之後,我們決定先用第二種方式試看看。主要原因是在現有的使用情境還不是太多的狀況之下,實作的成本應該不會太大。但介面的封裝和靈活度對我們來說卻是不管怎樣都是重要的,因為它代表了合作的介面和溝通的成本。

現有的 app 是由 React + Redux 為底建構而成, 已經有大量的使用 RESTful API。第一步的目標是把一個頁面中的某個區塊的資料,改成透過用 GraphQL 來取得。最基本需要做到的事情包含:

  • 透過 GraphQL 拿回來的 data 要能夠和 Redux 和平共處,同時存在。對於 presentational components 來說,介面上應該不會有任何的不同。
  • 遵循 React pattern,declaratively 的去和 GraphQL client 互動,像是 render 一個 component,或是 dispatch an action。讓整個 app 的可測試性保持在一定的水準。
  • 我們用 JWT 來驗證 user 的身分,所以預期中的 GraphQL client 要能夠自動的 re-auth & retry 失敗的 requests。如果能夠 reuse 現有的 redux middleware 會更理想。
  • 需要能夠操作 request 的 header 來滿足backend 的 security check。

在做一開始的 research 時很快的就找到了一個相對成熟的工具 Apollo。很幸運的是它的介面設計和原則非常上道,如同它所陳述:

Apollo design principles:

- Incrementally adoptable

- Universally compatible

- Simple to get started with

Apollo 提供了一個 React component <Query />,讓我們能夠用 props 來決定 query,用 renderProps 來拿到 request results。最終我們在 container layer 的 render method 裡,把來自 reducer 和 <Query /> 的資料整理後在 pass 給底下其他的 components。

另外我們夠透過足夠彈性的 plugin system apollo-link ,來客製完成我們對於 request 所需要做的額外操作。

在思考導入的方法的時候,也發現了通常一個技術不會是全部的方面都好(不然就不用選了阿,大家都來用就好了)。要意識到這點,先從需求出發,進而找到對我們最有利的實作方式。過程中也給我們一個機會認真的問自己:那什麼對我們來說是最重要的呢?

長遠來看,GraphQL 可能只是我們導入的 n 種技術的其中一種。但這次的過程從 “本來不需要” 到 “因為整個產品和團隊的前進,所以有新的需求,所以需要” 的過程我覺得對整個工程/產品團隊是一個珍貴的學習。因為在問題發生的當下,一切很可能會是混亂的(雖然說事後整理起來好像很直覺 XD)。在一次次的自我檢視之後慢慢定義出問題,進而找尋解法,是一個美妙的過程。

如果想要和我們一起體驗上面美妙的過程的話(?),歡迎來跟我們聊天

Inside Codementor

Inside Codementor

Yang Hsing Lin

Written by

Inside Codementor

Inside Codementor