今日主題
之前在 GraphQL 入門: 實作 Custom Scalar Type (Date Scalar Type) 這篇有介紹過 custom Scalar Type 。簡單來說就是可以自訂義更精確語意的 scalar type 來取代預設的 Int, Float String, Boolean, ID 。
這是一個很好用的功能,不過在使用前也要先思考:這個欄位的值的語意真的夠特別嗎 ? 預設的 Scalar Type 真的無法表達嗎 ? 不能使用自定義 enum type 表達嗎 ?
目前自己使用經驗上有幾種情形適合用 custom scalar type :
時間格式真的很容易搞慘人。另外個人經驗上最好回傳 +0 時間,再交給前端做時區轉換會是比較簡單的做法。
JSON 格式也是蠻常見的 Scalar Type ,尤其適合那種「我就是要一大包資料」的欄位。
可以減少後端檢查邏輯。
儲存圖片網址、頁面網址之類的很好用。
客戶輸錯電子郵件的案例相信不少人並不陌生,不管是 gmail 拼成 gmial 或是 _ 打成全形 _ 。
建議 8: 如果欄位的值的語意夠特別,可以考慮使用 custom scalar type
接著介紹 non-null 這個令人又愛又恨的欄位,優點是可以強化 Schema 的驗證邏輯,但缺點也不少,比如
之前已經說過,修改一個 non-null 欄位意味著 breaking change 。預測未來的 Schema 變化是不實際的,把 nullable 欄位轉向 non-null 很容易,但要轉回來的成本會高出很多。
打個比方,有可能今天有個 User
type ,規定一定要有名字與生日,那假如有一天註冊方式改用 Oauth 或其他方式使得 name
非必要,或是要修改能看到 age
的權限等等,都會限制你的 Schema 。
而且因為前端會預期得到 non-null ,所以如果 deprecate 掉這個欄位,還是要提供 default 值。
type User {
id: ID!
email: String!
name: String!
age: Int!
}
回傳 non-null 欄位的資料時,如果該欄位因為得到 null 而失敗,會導致整個 Object 一起失敗。
用上面的例子,假如 User
其中一個 non-null 欄位驗證失敗,那即使其他欄位資料正確也會整個失敗,進而可能讓前端難以處理錯誤,甚至讓 UI 元件壞掉...
所以一開始設計時,能先不用 non-null 就先不用。真的適合的地方有:
id 也不給就有點天理不容了。
比如一則 Post 、一張 Order 一定是由一個 User 產生出來的!如果沒有 parent 可以回傳那該檢討的應該是後端的程式碼。
身為一個 Boolean ,最討厭的就是拿到 Null ,那到底是有還是沒有還是不存在呢 ? 因此建議 Boolean type 欄位都可以專門寫一個 field resolver 來回傳一個預設值。
[type!]
欄位。Non-null 跟 Array 最好的結合方式。畢竟你可以給我一個 null 或一個空 Array ,但回傳一個夾雜 Null 的 Array 真的會令人捏把冷汗。
建議 9: 只在真正必要時才使用 non-null 。
當 Schema 中某些欄位用不到時,先別急著移除它,搞不好還有某個 client 正在使用!這時候就適合加入 @deprecated (reason: String)
的欄位,可以看看 Documentation 中,搜尋 __Type
點進 __Field
可以看到
當你使用 @deprecated
時, GraphQL 會自動幫你標注此欄位已經不再被支援並且加上理由。這樣一來,即使還有 client 正在使用,被 deprecate 的欄位依然會提供值,不會直接造成 breaking change !
不過美中不足的是,目前 input type 並沒有 deprecate 相關的 spce ,所以尚不支援 @deprecated
(可以看到沒有 deprecate 相關的欄位)
建議 10: 不再支援的 field 請用
@deprecated(reason: "")
來做廢除。
GraphQL 的優點在於「 API 即文件」,組織良好的架構與精準的命名讓整個 API 易讀好用,但是也別忘了加上註解!畢竟再怎麼精美的程式放久了也會發臭,而註解文件正好可以維持程式的新鮮度!
在使用註解時,通常會使用三種符號
"""
多行文件註解。通常用於 object type 的註解,註解裡面支援 markdown 格式。"
單行文件註解。通常用於 field 的註解,但如果該 field 註解過多也可考慮改用 """
。#
純單行註解。 #
的文字不會被顯示在 documentation 中,僅供程式裡參考。"""
User Type
"""
type User {
"名字"
# 這個註解不會出現在 documentation 裡面
name: String
}
但當然不是每一個人都有那麼多時間每行都寫文件註解,而且只要架構良好且命名清楚的話不用註解也可以清楚表達。但仍有幾個情形推薦使用註解:
建議 11: 開發在走,註解要有。
講完了上述的 Query 設計技巧,讓我們來看看有什麼 Design Pattern 可以供我設計時參考。
Node ID Interface 一方面增加了結構的可讀性,讓人可以一眼了解這個物件的重要性與意義,另一方面前端也能針對物件的 id 做 caching 或其他管理,以增加效能與效率。
interface Node {
id: ID!
}
type User implements Node {
id: ID!
...
}
type Product impplements Node {
id: ID!
...
}
另外還可以開這麼一個 query field 來增加 Schema 的彈性。
type Query {
node(id: ID!): Node
}
建議 11: 重要的 business object 都實現 Node Interface
當一個欄位需要表達大量資料時,使用 Array 就會一次撈出大量資料,進而拖累你的效率,這時候你需要的是分頁的技巧。 Connection 模式是一個被廣泛使用的模式,與 Node Interface 一樣都是由 Relay 推出的一個標準。
所以當你預期資料量會非常大時,比如你逛商店時有數千件商品,就該考慮使用 Array 或是 Connection 。
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type UserEdge {
cursor: String!
node: User
}
type UserConnection {
edges: [UserEdge]!!
pageInfo: PageInfo!
total: Int
}
在 Relay 還有推出一個很好用 Viewer 模式。使用 Viewer 模式可以很切合地配合前端的需求,只要使用 query.viewer
就可以滿足一個使用者幾乎所有的資料需求。
此外 Viewer 模式讓你的權限管理更加方便,不必要一隻一隻 query 的檢查,只需要配合進來的 viewer 身份給予符合權限的資料即可,因此非常適合有多種使用者身份的系統。
type Viewer {
self: User
products: [Product]
orders: [Order]
cart: Cart
"推薦商品"
recommendedProducts: [Product]
}
講完這些了這些,還有最後一點要強調,那就是小心不要設計出 Anemic domain model (貧血資料模型) 。引用 Clean Code 說過,使用物件導向設計時,要分清楚「物件」與「資料結構」的差別。物件的目的是用來提供行為而非暴露資料 ; 資料結構是暴露資料而非提供行為。簡單來說,物件不是用來當個容器放資料,而是透過提供 function 去操作資料。
同樣的,在 GraphQL Schema 資料設計中,雖然總體來說是個資料結構,但有些時候並不貼近實際使用情況,舉個例子來說,假如今天有這樣一個 Schema:
type User {
id: ID!
name: String
posts: [Post]
}
而今天需要一個 User 資訊總覽頁面,除了基本資料外,還需要「貼文的數量」。這時候使用 posts
再去取得 [Post]
長度就顯得很多餘了,此時不妨加上一個 postCount: Int
欄位,反而更貼進使用需求!
又或者這個頁面還需要「文章總讚數」的欄位,與其一個一個 post 拿出來統計,倒不如直接新增 totalPostLikeCount: Int
來滿足前端需求!
別忘了, GraphQL 是為了前端的需求而生,而非依資料庫的資料結構來設計! 如果你是從 RESTful 轉來 GraphQL ,那建議你可以直接重新設計 GraphQL ,不要貪圖一時的方便把整套舊的搬過來,兩者的設計精神從一開始就不相同!
type User {
...
postCount: Int
totalPostLikeCoun: Int
}
建議 12: API 可以提供的不只是資料,也應該要提供商業邏輯層面的欄位,。複雜的計算應該交由後端來處理,而非散落各處。
不過還是要提醒,不要因為有了商業邏輯欄位而拿掉原先的純資料欄位,兩者並行可以為 Schema 帶來更大的彈性!
關於避免 Anemic Object ,我們將會在明天的 Mutation 有更多實際案例來演示!
明後天將會帶來 Mutation 相關的設計守則!
Reference: