GraphQL 系列文(一):透過 The Schema Definition Language (SDL) 撰寫 GraphQL Schema

SDL 是建立 GraphQL Schema 的語言,而 Schema 則是定義 GraphQL API 的重要骨幹,包含資料架構格式和型別。

在了解 Schema 之前,先來了解 GraphQL 是什麼。

GraphQL

GraphQL 是一個用來查詢 API 的語言 (Query Language),也是一個基於型別系統來執行及查詢的 server-side runtime。

GraphQL 是設計用來快速開發高彈性、對開發者友善的 API。在開發完 API 後,會自動建立出一個線上的 IDE,在這個 IDE 中,可以測試發送 request,並得到相關的 response。在這個 IDE 中,也可以看得到完整的 API 文件。這邊有 Github 提供的 範例 IDE,有興趣的可以玩玩看~

在上方的 IDE 中,可以看到版面被切分成三個區塊。左邊是供 client 端撰寫的 query。Query 區塊下方可以寫入 query 要帶上的變數,並設定 request header。若要執行 query,可以按上方的播放鍵執行,執行後得到的結果就會出現在中間的區塊。而最右邊的就是自動建立出來的 API 文件。

當 GraphQL 的服務架起來時,便可以接受 GraphQL query,開始進行驗證及執行相關操作。GraphQL 會先確認 query 的型別與內容是否符合 Schema,驗證成功後才會執行 API 的資料存取。除此之外,GraphQL 還可以串接 RESTful API,詳細可以參考 官方教學 - Wrapping a REST API in GraphQL

GraphQL 不依賴於任何特定的資料庫或是 storage engine,而是建立在開發者所撰寫出來的 code 及數據。透過 GraphQL 開發 API 時,首先要了解的就是他的 type System,以及該如何自定義型別 (define type),並依據每個 type 實作相對應的 resolver(可以理解成實作 API 的地方)。

所以接下來,就來看看建立 GraphQL 的 Type System 的 SDL。

存取方式

在 GraphQL 中,資料存取方式分成三種,分別是 query, mutation 跟 subscription。

  • query:查詢資料
  • mutation:新增、編輯、刪除資料
  • subscription:擺脫以往發送 request 後得到 response 的方式,而是以透過 websocket 的方式,client 端訂閱某個資料後,Server 端一有新的資料後就會主動發送給 client。

資料型別 Scalar Types

  1. 可以讓 Client 與 Server 的開發者對於資料格式有共同的認知
  2. 強迫 Client 送出正確格式的 query
  3. 強迫 Server 回覆正確格式的 response

預設 Scalar Types

資料型別預設分成五種,分別是:

  • Int:32-bit 整數
  • Float:double precision 雙精度的浮點數
  • String
  • Boolean
  • ID:ID 一般來說會以 string 方式呈現,但當前端輸入 id 為 “483109245” (string) 或是 483109245 (int) 都會被 GraphQL 所接受。

自訂 Scalar Types

除了以上的型別外,也可以自訂型別,常見的有 Date, URL, Email, JSON 等等。可以動手 實作,也可以透過 套件

Syntax 語法

在了解以上基礎後,接下來就可以直接來看看,如何透過 SDL 訂定 GraphQL 的 Schema。

Comment

SDL 的註解方式分成三種,分別是 #""""

  • #:SDL 單行註解方式,不會呈現在自動生成的 GraphQL 文件中。
1
# 這個單行註解不會出現在文件中
  • ":SDL 單行註解方式,會呈現在自動生成的 GraphQL 文件中,經常用來備註 field definition 。
1
" 這個單行註解會出現在文件中 "
  • """:SDL 多行註解方式,會呈現在自動生成的 GraphQL 文件中。
1
2
3
"""
這個多行註解會出現在文件中
"""

non-nullable !

! 在 SDL 當中,代表一個不能為 null 的值。GraphQL 會保證傳來的資料,不會是 null

  • String!:這個值不可為 null,而且 scalar type 是字串
  • [User!]:這個值可能為 null,但陣列中不可為 null,且陣列中的 object type 為 User
  • [User]!:這個值不可能為 null,但陣列中可為 null,且陣列中的 object type 為 User
  • [User!]!:這個值不可為 null,且陣列中也不可為 null,陣列中的 object type 為 User

值得注意的是,假設今天設定資料為 myField: [String!],那可以猜猜看,以下哪個是合法的資料,哪個是不合法的:

1
2
3
4
5
6
7
8
9
10
11
# 如果資料要求為 myField: [String!]
myField: null
myField: []
myField: ['a', 'b']
myField: ['a', null, 'b']

# 如果資料要求為 myField: [String]!
myField: null
myField: []
myField: ['a', 'b']
myField: ['a', null, 'b']

為了不爆雷,答案在最下方,可以先想一下有答案後,再往下滑。

directive 指令

  • directive 指令以 @ 宣告
  • 是一種語法糖
  • 可以 custom directive
  • 原生有三個指令,一個用在 schema 就是 @deprecated(reason: String!),另外兩個指令可參照 如何透過 GraphQL 存取資料 - Query
    • @deprecated(reason: String!):schema 使用,是用來呈現在文件上,告訴 client 端盡量不要存取該欄位的用法,因此一定需要帶上 reason 的值。
1
2
3
4
type User {
"體重"
weight(unit: WeightUnit = KILOGRAM): Float @deprecated (reason: "It's secret.")
}

Schema

在還不太清楚前,可以先簡略的將 GraphQL Schema 想像成 DB Schema。一個 DB 可以有多個 Table,而一個 Schema 可以有多個 type;DB 中的 table 中有多個欄位,可分別設置資料型別,而 GraphQL type 也可以有多個 field (欄位),每個欄位的資料也可設定型別。

GraphQL 中的語法關鍵字 Schema 被官方稱為 Root Types,可以理解成所有存取資料的 entry point 進入點。在上面我們有提到存取方式有三種,因此 Schema 最多也只會包含三種類型:

1
2
3
4
5
6
7
8
schema {
"查詢"
query: Query
"新增、修改、刪除"
mutation: Mutation
"訂閱"
subscription: Subscription
}

Type / Field

type 是宣告 Object Type 的關鍵字。
Object Type 中若有可選的欄位,則是 Field

底下範例中:

  • Query 是 Object Type 的名字,供查詢。
  • 設定可以查詢的 Field 欄位被稱為 Field Names,這邊有有四個,分別是 hello, me, users, user。
  • string User User! [User!]! 被稱作 Field Types。
  • users 這個 Field Name 需要帶上 argument 參數 name,詳見以下 argument。
1
2
3
4
5
6
type Query {
hello: String # 資料格式是字串
me: User # 資料指向另一個 Object Type,此 Object Type 命名為 User
user(name: String!): User! # 資料指向另一個 Object Type User,而且不會是空值。 () 內的是參數 argument,表示一定要帶上參數 name,且參數值一定要是不得為空的字串
users: [User!]! # 資料不會是空值,且內容是一個陣列,此陣列內也不會是空值,陣列內容指向 User
}

interface

透過 interface 關鍵字宣告介面。

在以下範例中 Character 是這個介面名稱,實作此介面的時候都必須包含以下三個欄位。(個人認為有點類似 OOP 的多型)。

1
2
3
4
5
interface Character {
id: ID!
name: String
avatarUrl: String
}

implements

當要透過 type 實作出 interface 時,都需要透過 implements 這個關鍵字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"""
使用者相關資料
"""
type User implements Character {
id: ID!
name: String
avatarUrl: String
email: String
age: Int @deprecated(reason: "Use birthday") # 已被 deprecated
height(unit: HeightUnit = CENTIMETER): Float # 帶有 arguments 參數 unit,參數型別為 HeightUnit,詳見以下 enum,此參數預設值是 CENTIMETER
weight(unit: WeightUnit = KILOGRAM): Float # 帶有 arguments 參數 unit,參數型別為 WeightUnit,詳見以下 enum,此參數預設值是 KILOGRAM
friends: [User]
posts: [Post]
birthday: String
}

"""
粉專相關資料
"""
type FanPage implements Character {
id: ID!
name: String
avatarUrl: String
likeGivers: [User]
}

enum

受限的選項,常用在參數、Field Types 等。

在以下的範例中,型別為 WeightUnit 的,value 只有可能有三種可能,分別是 KILOGRAMGRAMPOUND等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""
體重單位
"""
# 代表值只有三種可能,分別是
enum WeightUnit {
KILOGRAM
GRAM
POUND
}

"""
身高單位
"""
enum HeightUnit {
METRE
CENTIMETRE
FOOT
}

union

可以理解成 type 的集合,當今天回傳資料類型,可能包含一個以上的 type 時,union 就非常適合。

以下範例來說,會依照參數需求,來回傳需要的設備是使用手機還是電腦,這時候就很適合使用 union 回傳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
union Device = Mobile | PC

type Mobile {
id: String!
price: Float!
}

type PC {
id: String!
price: Float!
}

type Query {
getDevice(need: String!): [Device]
}

argument / input

參數,經常使用在 query 及 mutation 中。
input 是宣告 input 這個 Object Type 的關鍵字。

參數數量若大於 3 個的話,通常會包成一個 input。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
type Mutation {
"新增使用者資訊,參數使用 input Object Type,一定要放參數"
addUser(input: addUserInput!): addUserPayload # 自定義回傳 payload
"編輯使用者資訊"
updateUser(input: updateUserInput!): User
"新增貼文資訊,參數小於三個,直接放裡面,title 選填,author 必填"
addPost(title: string, author: string!): Post
}

"""
新增使用者參數
"""
input addUserInput {
name: String!
email: String!
age: Int
}

"""
更改使用者資料參數
"""
input updateUserInput {
id: ID!
name: String
email: String
age: Int
}

"""
新增使用者後的回傳資料
"""
type addUserPayload {
addUser: User
"商業邏輯層的錯誤"
error: [UserError!]!
"執行時間"
timestamp: String
}

"""
自定義 Error 格式
"""
type UserError {
code: Int
message: String!
}

Answer

1
2
3
4
5
6
7
8
9
10
11
# 如果資料要求為 myField: [String!]
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error

# 如果資料要求為 myField: [String]!
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid

Reference

  1. 官網
  2. Think in GraphQL 系列文

評論

無法加載 Disqus 評論系統,請確保您的網絡能夠正常訪問。