0%

GraphQL 查詢語言 (1)

SQL,或結構化查詢語言 (Structured Query Language),是一種用來存取、管理與操作資料庫內資料的領域專用語言 (domain-specific language)。 SQL 導入 “以單一指令來存取多筆紀錄” 的概念,也可以讓你用任何鍵來存取任何紀錄,而不只是只能使用 ID。

SQL 可執行的指令相當簡單(喔?): SELECT、INSERT、UPDATE 與 DELETE。你只能對資料做這些事情。使用 SQL 時,我們可以編寫一條查詢指令來取得在資料庫的資料表內互相連接(join)的資料。

“資料只能讀取、建立、更新或刪除” 這個概念確實成就了 REST,這個概念要求我們基於這四種基本的資料操作法來使用不同的 HTTP 方法: GET、POST、PUT 與 DELETE。但是,如果你想要指定用 REST 來讀取或改變那一種類型的資料,唯一的方式就是透過端點 URL,不能用實際的查詢語言。

GraphQL 將原本用來查詢資料庫的概念應用在網際網路上。你只要用一種 GraphQL query 就可以回傳彼此相連的資料。你可以像 SQL 那樣使用 GraphQL query 來改變或移除資料。畢竟,SQL 與 GraphQL 代表同一個東西: Query Language (查詢語言)。

雖然 GraphQL 與 SQL 都是查詢語言,但它們完全不同,它們適用於完全不同的環境。 SQL query 是傳給資料庫的,而 GraphQL query 是傳給 API 的。SQL 資料存放在資料庫的資料表內,GraphQL 資料可存放在任何地方: 資料庫、多個資料庫、檔案系統、REST API、WebSocket,甚至其他的 GraphQL API。 SQL 是資料庫的查詢語言, GraphQL 是網際網路的查詢語言

GraphQL 與 SQL 的語法也完全不同。GraphQL 使用 Query 來請求資料,而不是 SELECT,這項操作是 GraphQL 完成的每項工作的核心。 GraphQL 將所有的資料的異動都包成一種資料型態: Mutation,而不是使用 INSERT、UPDATE 或 DELETE。因為 GraphQL 是讓網際網路使用的,它有一種訂閱(Subscription)型態可用來監聽通訊端連結上的資料變動。SQL 沒有類似訂閱的東西。

GraphQL 是按照規格來標準化的。無論你使用哪一種程式語言都可以: GraphQL 查詢單純是個 GraphQL 查詢。查詢語法是個字串,無論你的專案使用 JavaScript、Java、C# 或任何其他東西,它看起來都一樣。

GraphQL 的 query 只是放在 POST 請求的內文(body)中送給 GraphQL 端點的字串。下面是一個 GraphQL query,它是用 GraphQL 查詢語言寫成的字串:

query
1
2
3
4
5
{
allLifts {
name
}
}

你可以使用 curl 將這個 query 送給一個 GraphQL 端點:

1
2
3
curl 'http://snowtooth.herokuapp.com/'
-H 'Content-Type: application/json'
--data '{"query":"{allLifts {name}}"}'

或從瀏覽器使用 JavaScript:

1
2
3
4
5
6
7
fetch("http://snowtooth.herokuapp.com/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "{allLifts {name}}"})
})
.then(res => res.json())
.then(result => console.log(result));

如果 GraphQL schema 支援這種外形的 query,你就可以直接收到一個 JSON 回應。那個 JSON 回應有你請求的 data 欄位中的資料,或者,在發生錯誤時有個 errors 欄位。我們發出一個請求,收到一個回應。

若要修改資料,你可以傳送 mutation (變動)。mutation 長得很像 query,但它的目的是改變關於 app 整體狀態的事務。你可以使用 mutation 直接傳送執行改變所需的資料,例如:

mutation
1
2
3
4
5
6
mutation {
setLiftStatus(id: "panorama" status: OPEN) {
name
status
}
}

上面的 mutation 是用 GraphQL 查詢語言寫成的,我們希望用它將 id 為 panorama 的纜椅改為 OPEN 狀態。我們一樣可用 cURL 將這項操作送給 GraphQL 伺服器:

1
2
3
curl 'http://snowtooth.herokuapp.com/'
-H 'Content-Type: application/json'
--data '{"query":"mutation {setLiftStatus(id: \"panorama\" status:OPEN) {name status}}"}'

稍後我們將會看到如何使用更好的方法將變數對應到 query 或 mutation。

GraphQL API 工具

GraphQL 社群建立了一些可用來和 GraphQL API 互動的開放原始碼工具。這些工具可讓你用 GraphQL 查詢語言來編寫 query,並將這些 query 送到 GraphQL 端點,以及查看 JSON 回應。目前有兩種熱門工具: GraphiQL 與 GraphQL Playground。

GraphiQL

GrahpiQL 是 Facebook 建造的瀏覽器內部整合開發環境 (IDE),可用來查詢與瀏覽 GraphQL API。GraphiQL 提供語法突顯、程式自動完成及錯誤警告等功能,可以讓你直接在瀏覽器中執行查詢並查看查詢結果。許多公用的 API 都提供 GraphiQL 介面以供查詢即時資料。

GraphQL Playground

另一種可用來流覽 GraphQL API 的工具是 GraphQL Playground,它很像 GraphiQL,但它還有一些方便的功能,其中最重要的功能是它可以讓你連同 GraphQL 請求一起傳送自訂的 HTTP 標頭 (HTTP headers)。

GraphQL 查詢

Snowtooth Mountain 是虛構的滑雪勝地。我們將使用它來瞭解一些 GraphQL Query 範例。我們將要觀察 Snowtooth Mountain 網路團隊如何使用 GraphQL 來提供即時、最新的纜椅和雪道狀態資訊。Snowtooth 滑雪巡邏員可以直接用它們的手機打開與關閉纜椅和雪道。你可以參考 Snowtooth 的 GraphQL Playground 介面(請用 Chrome 瀏覽器)來了解往後的範例。

你可以使用 query (查詢)從 GraphQL API 請求資料。query 描述你想要從 GraphQL 伺服器取得的資料。當你傳送 query 時,就是以 field (欄位)為單位索取資料。這些欄位對應伺服器回傳的 JSON 資料回應內的同一組欄位。例如,如果你傳送一個索取 allLifts 的 query,並請求 name 與 status 欄位,就會收到一個含有 allLifts 陣列與每一個纜椅的 name 與 status 的字串。

query
1
2
3
4
5
6
query {
allLifts {
name
status
}
}
results
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"data": {
"allLifts": [
{
"name": "Astra Express",
"status": "CLOSED"
},
{
"name": "Jazz Cat",
"status": "CLOSED"
},
...
]
}
}
錯誤處裡

當你成功發出查詢後,會收到一個包含有 “data” 鍵的 JSON 文件。不成功的查詢會收到一個含有 “error” 鍵的 JSON 文件。這個鍵底下的 JSON 資料就是錯誤的詳細訊息。JSON 回應也可能同時含有 “data” 與 “error”。

你可以在查詢文件裡加入多個 query,但每次只能執行一項操作。

query
1
2
3
4
5
6
7
8
9
10
11
12
13
query lifts {
allLifts {
name
status
}
}

query trails {
allTrails {
name
difficulty
}
}

當你按下撥放按鈕時,GraphQL Playground 會要求你選擇這兩項操作之一。如果你想要藉由一個請求來取得所有資料,就必須將它們全都放在ㄧ個 query 裡面:

query
1
2
3
4
5
6
7
8
9
10
11
query liftsAndTrails {
liftCount(status: OPEN)
allLifts {
name
status
}
allTrails {
name
difficulty
}
}

從這裡開始,你可以慢慢看到 GraphQL 的優點。我們可以用一個 query 來索取各種不同的資料點。我們索取目前處於特定狀態的 liftCount、取得目前處於哪種狀態的纜椅量,也索取每一個纜椅的 name 與 status。最後,我們在同一個 query 中要求取得每一條滑雪道的 name 與 difficulty。

Query 是一種 GraphQL 型態。我們稱它為根型態,因為這種型態對應一項操作 (Operation),而操作是查詢文件的根。query 可在 GraphQL API 中使用的欄位已被定義在該 API 的 schema 裡面了,這個文件告訴我們哪個 Query 型態有哪些欄位可以選擇。

GraphQL Schema 告訴我們,當我們查詢這個 API 時,可以選擇欄位 liftCount、allLifts 與 allTrails。它也定義了其他可供選擇的欄位,但與查詢有關的重點在於,我們能夠選擇需要的欄位,以及省略不想要的欄位。

我們會在編寫 query 時,將需要的欄位放在大括號裡面來選擇它們,這些段落稱為選擇組 (selection set)。我們在選擇組中定義的欄位與 GraphQL 型態有直接的關係。liftCount、allLifts 與 allTrails 欄位都被定義在 Query 型態內。

你可以在ㄧ個選擇組裡面放入另一個選擇組。因為 allLifts 欄位會回傳一串 Lift 型態,我們必須使用大括號來建立一個這種型態的新選擇組。我們可以請求各種關於 lift 的資料,但在這個範例中,我們只想要選擇 lift 的 name 與 status。類似的情況,allTrails query 會回傳 Trail 型態。

JSON 回應含有我們在 query 中請求的所有資料,那些資料都會被格式化為 JSON,並且使用與 query 一樣的外形來傳遞。它發出的每一個 JSON 欄位的名稱都與選擇組的欄位名稱一樣。我們可以在 query 中指定別名(chairlifts, liftName)來改變回應物件的欄位名稱,例如:

query
1
2
3
4
5
6
7
8
9
10
11
query liftAndTrails {
open: liftCount(status: OPEN)
chairlifts: allLifts {
liftName: name
status
}
skiSlopes: allTrails {
name
difficulty
}
}

其回應為:

result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"data": {
"open": 6,
"chairlifts": [
{
"liftName": "Astra Express",
"status": "CLOSED"
}
],
"skiSlopes": [
{
"name": "Blue Bird",
"difficulty": "intermediate"
}
]
}
}

我們取回具備同樣外形的資料了,但也改變回應中的一些欄位名稱。要過濾 GraphQL 查詢結果,其中一種方式就是傳入查詢引數。引數是與一個查詢欄位有關的一對鍵/值(或好幾對)。如果我們只想要取得被關閉纜椅的名稱,可以傳入一個過濾回應的引數:

query
1
2
3
4
5
6
query closedLifts {
allLifts(status: CLOSED) {
name
status
}
}
query
1
2
3
4
5
6
7
8
query jazzCatStatus {
Lift(id: "jazz-cat") {
name
status
night
elevationGain
}
}

邊 (edge) 與連結

在 GraphQL 查詢語言中,欄位可為純量型態物件型態。純量型態 (scalar type)類似其他語言中的基本型態。他們是選擇組的葉(leaf)節點。GraphQL 內建五種純量型態: 整數 (Int)、浮點數 (Float)、字串 (String)、布林 (Boolean)、及專屬代碼 (ID)。

使用整數與浮點數都會得到 JSON 數字,使用字串與 ID 則會得到 JSON 字串。使用布林只會得到布林值。雖然使用 ID 與 String 字串會得到同一種 JSON 資料型態,但 GraphQL 會確保 ID 回傳唯一的字串。

GraphQL 物件型態是在 schema 內定義的一或多個欄位群組,它們定義了應回傳的 JSON 物件的外形。JSON 可以在欄位底下無止盡地嵌套物件,GraphQL 也是如此。我們可以藉由查詢某個物件來取得與它有關的物件的細節來將物件連(join)在一起。

例如,假如我們想要取得可以用特定的纜椅到達雪道的清單:

1
2
3
4
5
6
7
8
9
10
11
query trailsAccessedByJazzCat {
Lift(id: "jazz-cat") {
name
status
capacity
trailAccess {
name
difficulty
}
}
}

在上述的 query 中,我們索取關於 “Jazz Cat” 纜椅的一些資料,name、status 與 capacity 欄位都是純量型態。trailAccess 欄位是屬於 Trail 型態(物件型態)。在這個範例中,trailAccess 會回傳一個過濾過的、可用 Jazz Cat 抵達的雪道清單。因為 trailAccess 是 Lift 型態的欄位,API 可使用父物件(也就是 Jazz Cat Lift)的資料來過濾回傳的雪道清單。

這個操作範例查詢兩種資料型態(纜椅與雪道)之間的一對多連結。一台纜椅與許多與它有關的雪道相連。如果我們從 Lift 節點開始遍歷圖,可透過命名為 trailAccess 的邊前往與該纜椅連接的一或多個 Trail 節點。如果你要從 Trail 節點走回 Lift 節點,因為這張圖是無向的,所以可以做到:

1
2
3
4
5
6
7
8
9
query liftToAccessTrail {
Trail(id: "dance-fight") {
groomed
accessedByLifts {
name
capacity
}
}
}

在 liftToAccessTrail query 中,我們選擇一個名為 “Dance Fight” 的 Trail。groomed 欄位回傳一個布林純量型態,可讓我們知道 Dance Fight 雪道是否被整理過了。accessedByLifts 欄位回傳可帶著滑雪客前往 Dance Fight 雪道的纜椅。

接續下一篇 GraphQL 查詢語言 (2)