0%

GraphQL 查詢語言 (2)

Fragment

你可以將操作的定義及 fragment 放入 GraphQL 查詢文件。fragment 是可在多個操作中重複使用的選擇組(SelectionSet)。我們回到 Snowtooth 的 GraphQL Playground 介面,看一下這個 query:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
query {
Lift(id: "jazz-cat") {
name
status
capacity
night
elevationGain
trailAccess {
name
difficulty
}
}
Trail(id: "river-run") {
name
difficulty
accessedByLifts {
name
status
capacity
night
elevationGain
}
}
}

這個查詢請求的是 Jazz Cat 纜車與 River Run 雪道的資訊。Lift 的選擇組裡有 name、status、capacity、night 與 elevationGain。我們想要取得的 River Run 雪道資訊有一些欄位與 Lift 型態的欄位相同。我們可以建立一個 fragment 來協助減少 query 重複的地方:

1
2
3
4
5
6
7
fragment liftInfo on Lift {
name
status
capacity
night
elevationGain
}

我們用 fragment 關鍵字來建立 fragment。fragment 是屬於特定型態的選擇組,所以你必須在 fragment 的定義中指定它所屬的型態。這個範例的 fragment 稱為 liftInfo,它是 Lift 型態的選擇組。

當我們要在另一個選擇組中加入 liftInfo fragment 欄位時,可在 fragment 名詞前面加上三個句點:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
query {
Lift(id: "jazz-cat") {
...liftInfo
trailAccess {
name
difficulty
}
}
Trail(id: "river-run") {
name
difficulty
accessedByLifts {
...liftInfo
}
}
}

fragment liftInfo on Lift {
name
status
capacity
night
elevationGain
}

這種語法類似 JavaScript 的 spread 運算子。這三個句點可讓 GraphQL 將 fragment 的欄位指派給當前的選擇組。在這個範例中,我們在 query 的兩個地方使用同一個 fragment 來選擇 name、status、capacity、night 與 elevationGain。

我們無法將 liftInfo fragment 加入 Trail 選擇組,因為 liftInfo 只定義了 Lift 型態的欄位。我們可以加入另一個雪道 Trail 型態欄位的 fragment trailInfo:

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
query {
Lift(id: "jazz-cat") {
...liftInfo
trailAccess {
...trailInfo
}
}
Trail(id: "river-run") {
...trailInfo
groomed
trees
night
}
}

fragment trailInfo on Trail {
name
difficulty
accessedByLifts {
...liftInfo
}
}

fragment liftInfo on Lift {
name
status
capacity
night
elevationGain
}

在這個範例中,我們建立了一個稱為 trailInfo 的 fragment,並在 query 的兩個地方使用它。我們也在 trailInfo fragment 中使用 liftInfo fragment 來選擇與它連接的纜椅資料。你可以視需求建立任意數量的 fragment,並交換使用它們。在 River Run Trail query 使用的選擇組中,我們將 fragment 與想要選擇的、關於 River Run 雪道的其他資料結合。你可以一併使用 fragment 與選擇組的其他欄位,也可以在單個選擇組中結合多個屬於同樣型態的 fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
query {
allTrails {
...trailStatus
...trailDetails
}
}

fragment trailStatus on Trail {
name
status
}

fragment trailDetails on Trail {
groomed
trees
night
}

fragment 有個很棒的優點在於,你只需要修改一個 fragment,就可以修改在許多不同的 query 裡面的選擇組:

1
2
3
4
fragment liftInfo on Lift {
name
status
}

這樣修改 liftInfo fragment 的選擇組會讓使用這個 fragment 的每一個 query 選擇較少的資料。

變動 Mutation

到目前為止,我們已討論許多關於讀取資料的事情了。我們用 query 來描述在 GraphQL 中發生的所有讀取。若要寫入新資料,就要使用 mutation (變動)。mutation 的定義類似 query ,它們都有名稱,也可以擁有 “可回傳物件型態或純量型態的選擇組” ,不同之處在於 mutation 可執行一些影響後端資料狀態的修改。

例如,這是個很危險的 mutation:

1
2
3
mutation burnItDown {
deleteAllData
}

Mutation 是一種根物件型態。API 的 schema 定義了這個型態可用的欄位 (field)。上述的 Mutation API 擁有強大的威力,可清除所有的資料,它實作了一個 deleteAllData 的欄位,當所有資料都被成功的刪除,這個欄位會回傳一個純量型態: true,或者如果出錯了,會回傳 false。資料究竟會不會被刪除是藉由實作這個 API 來決定。

我們來看另外一個範例,這次我們要創造一些東西,而不是摧毀東西:

1
2
3
4
5
6
7
mutation createSong {
addSong(title: "No Scrubs", numberOne: true, performerName: "TLC") {
id
title
numberOne
}
}

我們可以使用這個範例來創建新歌曲,利用引數將 title、numberOne 狀態與 performerName 傳給這個 mutation 之後,它會將這首新歌加入資料庫。如果這個 mutation 欄位 addSong 會回傳物件,你就要在這個 mutation 後面加入一個選擇組。在本例中,mutation 完成後會回傳 Song 型態的物件,裡面有剛才創建的歌曲的資料。我們可以在 mutation 後面選擇新歌的 id、title 與 numberOne 狀態的選擇組:

1
2
3
4
5
6
7
8
9
{
"data": {
"addSong": {
"id": "Saca534f4bb1de07cb6d73ae",
"title": "No Scrubs",
"numberOne": true
}
}
}

上面是這個 mutation 可能的回應。如果出錯,這個 mutation 會在 JSON 回應裡面回傳錯誤,而不是新建的 Song 物件。

我們也可以使用 mutation 來更改既有的資料。當我們想要更改 Snowtooth 的纜椅狀態時,也可以使用 mutation:

1
2
3
4
5
6
mutation closeLift {
setLiftStatus(id: "jazz-cat" status: CLOSED) {
name
status
}
}

這個 mutation 將 Jazz Cat 纜椅的狀態從 open 改成 closed。我們可以在 mutation 後面的選擇組裡面選擇被更改的 Lift 的欄位。在此,我們取得被改變的纜椅的 name,以及更新後的 status。

使用查詢變數

現在我們已經會使用 mutation 引數傳送新字串值來更改資料了,另一種做法是使用輸入變數,以變數取代 query 內的靜態值可讓我們可以傳入動態值。在 addSong mutation,我們要用變數名稱來取代字串,在 GraphQL 中,變數必定以 $ 字元開頭:

1
2
3
4
5
6
7
mutation createSong($title:String! $numberOne:Boolean $by:String!) {
addSong(title:$title, numberOne:$numberOne, performerName:$by) {
id
title
numberOne
}
}

我們將靜態值換成 $variable,接著宣告的 mutation 可接收 $variable。GraphiQL 與 Playground 都有一個 Query Variables 視窗 (左下角,可把空間往上拉大),我們在這裡用 JSON 物件來傳送輸入資料,務必用正確的變數名稱作為 JSON 鍵:

1
2
3
4
5
{
"title": "No Scrubs",
"numberOne": true,
"by": "TLC"
}

當你傳送引數資料時,變數的功能很強大,它不但可讓你的 mutation 在測試的過程中更有條理,當你連接用戶端介面時,使用動態輸入也有很大的幫助。

訂閱 Subscription

GraphQL 的第三種操作類型是訂閱 (subscription)。有時用戶端想要取得伺服器傳送的即時更新。訂閱可讓我們監聽 GraphQL API 的即時資料變更。

GraphQL 的訂閱功能來自 Facebook 的實際使用案例。這個團隊想要在不重新整理網頁的情況下,顯示關於貼文獲得的贊 (Live Likes)數量的即時資訊。Live Likes 是以訂閱來製作的及時使用案例。每一個用戶端都會訂閱 like 事件,並即時看到 like 的更新。

如同 mutation 與 query,subscription 是一種根型態。你必須在 API schema 的 subscription 型態下的欄位中定義用戶端可以監聽的資料變更。編寫 GraphQL query 來監聽 subscription 的做法類似定義其它操作的方式。

例如,在 Snowtooth 中,我們可以用 subscription 監聽任何纜椅狀態的變動:

1
2
3
4
5
6
7
subscription {
liftStatusChange {
name
capacity
status
}
}

當我們執行這個 subscription 時,會用 WebSocket 來監聽纜椅狀態的改變。請留意,在 GraphQL Playground 按下播放按鈕後不會立刻收到回傳的資料。當 subscription 被送到伺服器時,這個 subscription 會監聽資料的任何改變。

如果你想要看到 subscription 收到資料,就必須做出改變。你必須打開一個新視窗或標籤,用 mutation 來傳送改變。 如果使用的是 GraphQL Playground,視窗上端可以直接打開一個新標簽來加入 mutation。 我們在新視窗或標籤送一個改變纜椅狀態的 mutation:

1
2
3
4
5
6
mutation closeLift {
setLiftStatus(id: "astra-express" status: HOLD) {
name
status
}
}

回到 subscription 的視窗,將會收到被更新的資料,以及資料被送到 subscription 的時間。

與 query 和 mutation 不同的是,subscription 會保持開啟。接下來每當有纜椅的狀態改變時,新的資料就會被推送到這個 subscription。若要停止監聽狀態的變動,你必須取消 subscription。當你使用 GraphQL Playground 時,只要按下停止按鈕即可。不幸的是,用 GraphiQL 來取消 subscription 唯一的做法是關閉運行該 subscription 的瀏覽器標籤。

自我查詢 Introspection

自我查詢 (introspection)是 GraphQL 最強大的功能之一。自我查詢是指查詢目前的 API 的 schema。自我查詢是將這些精心建構的 GraphQL 文件加入 GraphiQL 與 Playground 介面的方式。

你可以傳送 query 給每一個 “可回傳特定 API 的 schema 資料” 的 GraphQL API。例如,如果你要知道可在 Snowtooth 中使用哪種 GraphQL 型態,可以執行 __schema query 來查看該資訊:

1
2
3
4
5
6
7
8
query {
__schema {
types {
name
description
}
}
}

當你執行這個 query 時,可以看到這個 API 可用的每一個型態,包括根型態、自訂型態,甚至純量型態。如果你想要查看特定型態的資料,可執行 __type query,並用引數來傳送想要查詢的型態名稱:

1
2
3
4
5
6
7
8
9
10
11
12
query liftDetails {
__type(name:"Lift") {
name
fields {
name
description
type {
name
}
}
}
}

這種自我查詢功能可讓你看到 Lift 型態可供查詢的所有欄位。當你想要瞭解新的 GraphQL API 時,找出根型態提供哪些欄位是很好的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
query roots {
__schema {
queryType {
...typeFields
}
mutationType {
...typeFields
}
subscriptionType {
...typeFields
}
}
}

fragment typeFields on __Type {
name
fields {
name
}
}

自我查詢會遵循 GraphQL 查詢語言的規則。我們用 fragment 來簡化上述的 query,並查詢每個根型態的名稱與它們提供的欄位。自我查詢可讓用戶端知道目前的 API schema 如何運作。

抽象語法樹 (Abstract syntax tree, AST)

query 文件是個字串。當我們傳送 query 給 GraphQL API 時,字串會被解析成抽象語法樹,並在操作執行之前進行驗證。抽象語法樹 AST 是一種代表 query 的階層式物件,是個含有內嵌欄位的物件,裡面的欄位代表 GraphQL query 的細節。

解析程序的第一個步驟是將字串解析成一堆較小的片段,這個步驟包括將關鍵字、引數,甚至括號與冒號解析成單獨的標記,這個程序稱為詞法分析 (lexing 或 lexical analysis)。接下來將詞法分析後的 query 解析成 AST。使用 AST 可讓動態修改與驗證 query 的工作輕鬆許多。

例如,你的 query 一開始是 GraphQL 文件。文件至少有一個定義,也可能有一串定義。定義只有可能是兩種型態之一: OperationDefinitionFragmentDefinition。下面的文件範例有三個定義: 兩項操作 (Operation) 與一個 fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
query jazzCatStatus {
Lift(id: "jazz-cat") {
name
night
elevationGain
trailAccess {
name
difficulty
}
}
}

mutation closeLift($lift: ID!) {
setLiftStatus(id: $lift, status: CLOSED) {
...liftStatus
}
}

fragment liftStatus on Lift {
name
status
}

一個 OperationDefinition 只能含有三種操作型態之一: mutationquerysubscription。每一個操作定義都有 OperationTypeSelectionSet

在每一個操作後面的大括號內都有該操作的 SelectionSet,它們就是我們用引數來查詢的欄位。例如,Lift 欄位是 jazzCatStatus query 的 SelectionSet,而 setLiftStatus 欄位是 closeLift mutation 的選擇組。

選擇組可嵌套在另一個選擇組裡。jazzCatStatus query 有三個嵌套的選擇組。第一個 SelectionSet 含有 Lift 欄位。在它裡面有個 SelectionSet 含有 name、night、elevationGain 與 trailAccess 欄位。在 trailAccess 欄位內有另一個 SelectionSet,含有每個雪道的 name 與 difficulty 欄位。

GraphQL 可遍歷這個 AST 並且用 GraphQL 語言與目前的 schema 來驗證它的細節。如果查詢語言的語法是正確的,且 schema 含有我們請求的欄位與型態,該操作就會執行。如果情況不是如此,就會回傳特定的錯誤。

AST 是 GraphQL 很重要的成分。每一項操作都會被解析成 AST,以便對它進行驗證並最終執行它。