前段時間組內搞代碼檢視,經常能看到一些 “掛着 RESTful 羊頭,賣的卻是 GraphQL 狗肉”的 API 設計。
舉個例子,假如後台有兩種資源用户 User 和 羣組 Group ,按照RESTful的規範,他們設計以下API端點:
# 獲取用户列表
GET /users
# 獲取指定用户
GET /user/{id}
# 創建用户
POST /users
# 修改用户
PUT /user/{id}
# 刪除用户
DELETE /user/{id}
# 獲取羣組列表
GET /groups
# 獲取指定羣組
GET /group/{id}
# 創建羣組
POST /groups
# 修改羣組
PUT /group/{id}
# 解散羣組
DELETE /group/{id}
咋一看沒啥問題,可是到了後面,要實現 “用户加入羣組” 和 “用户退出羣組” 兩個特性時,他們給出的API設計:
# 用户加入羣組
PATCH /group/{id}
{
"addMembers": ["userID"]
}
# 用户退出羣組
PATCH /group/{id}
{
"deleteMembers": ["userID"]
}
我當場就蚌埠住了,這哪裏是 RESTful 風格,這不就是他們老大深痛惡絕的 GraphQL ?
眼看我就要當場發飆,有個老手趕緊出來圓場,他給出以下API設計:
# 用户加入羣組
PATCH /user/{id}/group/{groupID}
# 用户退出羣組
DELETE /user/{id}/group/{groupID}
這個設計看起來是解決剛才的問題,但實際上只是掩耳盜鈴。
我接着追問: “管理員邀請用户加入羣組” 和 “管理員將用户踢出羣組” 要怎麼設計呢?
那個老手依瓢畫葫蘆給出以下設計:
# 管理員邀請用户加入羣組
PATCH /group/{id}/user/{userID}
# 管理員將用户踢出羣組
DELETE /group/{id}/user/{userID}
眼看他們還不醒悟,我就接着追問:“用户加入羣組需要管理員同意” 和 “管理員邀請用户加入羣組需要用户同意” 又怎麼實現呢?
這下子老手也沒轍,只能説這塊的代碼需要重新設計。
我讓他們把原來的設計文檔重新拿出來檢視,然後發現很多地方都不符合規範,連基本的實體關係圖ER都沒有!
我忍不住吐槽:怪不得當年我一個人拿着不到一半的工資,連前後端一起搞,效率卻比這幫985/211的高材生/海歸還要高,而他們連一個最基本的CURD都要不斷地返工!
原來招人的HR完全不考慮專業是否對口,只看畢業院校和學歷,結果這幫非科班出身的人連ER圖都不會,更別説更復雜一點的類圖、活動圖以及泳道圖,要知道對科班出身的人來説畫UML是基本功!
其實他們的設計裏面已經有了 User 和 Group 兩張實體表,再增加一張 Relation 關係表:
struct Relation {
id string
user_id string
group_id string
status string # 根據cookie的session獲取當前用户id
# 再判斷當前用户是不是當前group的管理員
# 如果是則管理員邀請用户,把status設置為"wait_for_admin_approve"
# 否則就是用户申請加入羣組,把status設置為"wait_for_user_accept"
}
然後這樣設計API:
# 用户加入羣組
POST /relations
{
user: "abc",
group: "ikun"
}
# 用户退出羣組
DELETE /relations/{id}
# 管理員邀請用户加入羣組
POST /relations
{
user: "abc",
group: "ikun"
}
# 管理員將用户踢出羣組
DELETE /relations/{id}
回頭總結,不難發現,大多數人在實踐RESTful規範時,最容易犯的錯誤就是處理嵌套資源的時候,容易設計出 /parent/xxx/sub/xxx 這樣的API。
這種設計咋一看起來確實非常容易理解,但其實後期非常難以維護,尤其是遇到 n:m 多對多的關係時,這種嵌套的API設計就是一場災難。
即便是 1:n 一對多的情況下,嵌套的API設計看起來沒有問題,但後期當子資源 n 越來越大的時候,單獨增加/刪除某個子資源需要把所有子資源都獲取一遍,就非常容易形成性能瓶頸,並且沒辦法通過分庫分表的方式進行橫向擴容。
儘管我們可以換成GraphQL的風格,通過 addSub 和 delSub 的方式單獨新增或刪除某個子資源,來避免每次都需要獲取所有的子資源。
但我們也必須要考慮併發更新資源頻繁引發衝突的風險;尤其越來越多的Java開發轉向基於Go的雲原生開發,為了方便維護不再額外引入SQL數據庫,直接使用etcd,但etcd本身也有mvcc機制來保證數據的一致性,這樣還是會導致併發更新資源的衝突!
經過這次事情,我也徹底明白了,真正掌握RESTful規範,離不開對關係模型的深厚功底;儘管我們從SQL數據庫切換到etcd這類NoSQL數據庫,按道理API設計規範應該相應地換成GraphQL與對象模型緊密相連的規範。
但考慮團隊水平參差不齊,後台API接口設計規範還是要堅持RESTful為主,先把基於關係數據庫的基本功練好!
前端團隊是可以基於GraphQL規範實現Backend for Frontend,搞一個接口聚合模塊,作為性能優化手段,減少首頁白屏時間。