查詢(Queries)
學習如何從 GraphQL 伺服器取得資料
GraphQL 支援三種主要的操作類型——查詢(queries)、變更(mutations)以及訂閱(subscriptions)。我們在本指南中已經看過幾個基本查詢的範例,這一頁會詳細介紹如何使用查詢操作來從伺服器讀取資料。
欄位(Fields)
在最簡單的情況下,GraphQL 就是針對物件要求特定的欄位。我們先來看看在 schema 中 Query
型別所定義的 hero
欄位:
type Query {
hero: Character
}
我們可以看看查詢這個欄位時會得到什麼結果:
# { "graphiql": true }
{
hero {
name
}
}
當我們建立一個 GraphQL 文件 時,總是從根操作型別(本例中為 Query
物件型別)開始,因為它是 API 的進入點。接著我們必須指定我們感興趣的欄位 選擇集,一路往下到最底層的值,這些值會是 Scalar 或 Enum 型別。name
欄位回傳的是 String
型別,在這個例子中是星際大戰主角的名字,"R2-D2"。
GraphQL 規範指出,請求的結果會回傳在回應的頂層 data
key 下。如果請求發生錯誤,會在頂層的 errors
key 提供錯誤資訊。你可以發現,結果的結構和查詢的結構相同。這是 GraphQL 的核心,因為你總是能得到你預期的資料,伺服器也能精確知道客戶端要求哪些欄位。
在前面的例子中,我們只要求了 hero 的名字,這會回傳一個 String
,但欄位也可以回傳物件型別(或其陣列)。這時你可以對該物件型別做 子選擇:
# { "graphiql": true }
{
hero {
name
friends {
name
}
}
}
GraphQL 查詢可以遍歷相關物件及其欄位,讓客戶端能在一次請求中取得大量相關資料,而不需要像傳統 REST 架構那樣多次往返請求。
請注意,在這個例子中,friends
欄位回傳的是一個項目陣列。GraphQL 查詢在單一項目或多項目時語法相同;不過我們可以根據 schema 的定義來判斷預期會拿到哪一種。
參數(Arguments)
如果我們只能遍歷物件及其欄位,GraphQL 已經是一個非常實用的資料查詢語言了。但當你可以為欄位傳遞參數時,事情就變得更有趣了:
type Query {
human(id: ID!): Human
}
客戶端必須在查詢時提供必要的 id
值:
# { "graphiql": true }
{
human(id: "1000") {
name
height
}
}
在像 REST 這樣的系統中,你只能傳遞一組參數——也就是請求中的查詢參數和 URL 片段。但在 GraphQL 中,每個欄位和巢狀物件都可以有自己的參數,這讓 GraphQL 完全可以取代多次 API 請求。
你甚至可以為回傳 Scalar 型別的欄位傳遞參數;一個常見用途是讓伺服器端統一處理資料轉換,而不是每個客戶端都要自己處理:
# { "graphiql": true }
{
human(id: "1000") {
name
height(unit: FOOT)
}
}
參數可以是多種不同的型別。在上面的例子中,我們使用了 Enum 型別,代表有限選項之一(這裡是長度單位,可以是 METER
或 FOOT
)。GraphQL 內建一組預設型別,但 GraphQL 伺服器也可以宣告自訂型別,只要能序列化成你 的傳輸格式即可。
操作型別與名稱(Operation type and name)
在上面的範例中,我們使用了一種簡寫語法,省略了在操作選擇集前面的 query
關鍵字。除了可以明確指定 操作型別 之外,我們也可以為操作加上一個獨特的 操作名稱,這在正式環境的應用程式中特別有用,因為這樣有助於除錯與追蹤。
以下是一個同時包含 query
關鍵字(操作型別)與 HeroNameAndFriends
(操作名稱)的範例:
# { "graphiql": true }
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
操作型別可以是 query
、mutation
或 subscription
,用來描述你想執行的操作類別。這個關鍵字在查詢時可以省略(只限查詢),但在變更與訂閱時則是必須的。如果你想為操作命名,也必須明確指定操作型別。
操作名稱是你為操作自訂的名稱,建議取一個有意義的名字。當你在一份文件中傳送多個操作時,操作名稱是必須的;即使只傳送一個操作,也建議加上名稱,因為這對除錯與伺服器端日誌紀錄都很有幫助。當出現錯誤(不論是在網路日誌還是 GraphQL 伺服器的日誌中),你可以更容易地根據名稱在程式碼中找到對應的查詢,而不用去猜查詢內容。
別名(Aliases)
如果你很細心,可能已經注意到,因為回傳結果的欄位名稱會和查詢中的欄位名稱相同,但不包含參數,所以你無法直接用不同參數查詢同一個欄位。這時你就需要別名——它可以讓你把欄位的回傳結果重新命名成你想要的名稱。
# { "graphiql": true }
query {
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
在上面的例子中,兩個 hero
欄位本來會衝突,但因為我們可以用別名把它們改成不同的名稱,就能在同一次請求中取得兩個結果。
變數(Variables)
到目前為止,我們都是直接在查詢字串中寫死所有參數。但在大多數應用程式中,欄位的參數通常是動態的。例如,可能有一個下拉選單讓你選擇想看的星際大戰集數,或是一個搜尋欄位,或是一組篩選條件。
如果把這些動態參數直接寫在查詢字串裡,客戶端程式就必須在執行時動態操作查詢字串,並序列化成 GraphQL 特有的格式。為了解決這個問題,GraphQL 提供了第一級的變數支援,可以把動態值從查詢中抽離,另外用一個物件傳遞。這些值就叫做變數。
當我們開始使用變數時,需要做三件事:
- 把查詢裡的靜態值換成
$變數名稱
- 在查詢中宣告
$變數名稱
為可接受的變數 - 在另外一個(通常是 JSON)變數物件裡傳入
變數名稱: 值
全部加起來會像這樣:
# { "graphiql": true, "variables": { "episode": "JEDI" } }
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
你必須在 GraphQL 文件中指定操作型別與名稱,才能使用變數。
現在,在客戶端程式碼中,我們只需要傳入不同的變數值,而不用每次都組一個新的查詢字串。一般來說,這也是一個很好的習慣,可以明確標示哪些查詢參數是動態的——我們永遠不應該用字串插值的方式,把使用者輸入直接拼進查詢字串裡。
變數定義(Variable definitions)
變數定義就是像上面查詢裡 ($episode: Episode)
這一段。它的用法就像型別語言中函式的參數定義一樣。它會列出所有變數,前面加上 $
,後面接型別,例如這裡的 Episode
。
所有宣告的變數必須是 Scalar、Enum 或 Input Object 型別。所以如果你想傳遞複雜物件給欄位,必須知道伺服器上對應的 input type。
變數定義可以是選填或必填。像上面例子,因為 Episode
型別後面沒有 !
,所以是選填。如果你要把變數傳給一個必填參數,那變數本身也必須是必填。
想更了解這些變數定義的語法,可以參考 Schemas and Types 頁面。
預設變數值(Default variables)
你也可以在查詢裡為變數加上預設值,只要在型別宣告後面加上預設值即可:
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
當所有變數都有預設值時,你可以不帶任何變數就呼叫查詢。如果有傳變數,則會覆蓋預設值。
片段(Fragments)
假設我們的應用程式有一個比較複雜的頁面,可以讓我們同時比較兩個英雄以及他們的朋友。你可以想像,這樣的查詢很快就會變得很冗長,因為至少要重複一次欄位——每一邊都要寫一次。
這就是為什麼 GraphQL 有可重複使用的單位,叫做片段(fragments)。片段讓你可以先定義一組欄位,然後在需要的查詢裡引用。以下是一個用片段解決上述情境的範例:
# { "graphiql": true }
query {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
你可以想像,如果不能用片段,這個查詢會有多冗長。片段的概念常被用來把複雜的資料需求拆成小區塊,特別是當你需要把很多 UI 元件的片段組合成一次初始資料請求時。
在片段中使用變數(Using variables inside fragments)
片段也可以存取在操作中宣告的變數:
# { "graphiql": true }
query HeroComparison($first: Int = 3) {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
friendsConnection(first: $first) {
totalCount
edges {
node {
name
}
}
}
}
行內片段(Inline Fragments)
就像許多型別系統一樣,GraphQL schema 也可以定義 Interface 和 Union 型別。你可以在 Schemas and Types 頁面了解更多。
如果你查詢的欄位回傳的是 Interface 或 Union 型別,你就需要用 行內片段 來存取底層具體型別的資料。用範例最容易理解:
# { "graphiql": true, "variables": { "ep": "JEDI" } }
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
在這個查詢中,hero
欄位回傳 Character
型別,根據 episode
參數可能是 Human
或 Droid
。在直接選擇時,你只能查 Character
介面上的欄位,例如 name
。
如果要查詢具體型別的欄位 ,就要用帶型別條件的行內片段。像第一個片段標記為 ... on Droid
,只有當 hero
回傳的是 Droid
型別時,primaryFunction
欄位才會被執行。Human
型別的 height
欄位也是同理。
命名片段也可以這樣用,因為命名片段一定會指定型別。
中繼欄位(Meta fields)
如同我們在 Union 型別看到的,有時你不知道從 GraphQL 服務會拿到什麼型別的資料,因此需要一種方式讓客戶端判斷該怎麼處理。
GraphQL 允許你在查詢的任何地方要求 __typename
這個中繼欄位,來取得該處物件型別的名稱:
// ... existing code ...
在上面的查詢中,search
回傳的是 Union 型別,可能是三種型別之一。如果沒有 __typename
欄位,客戶端就無法分辨不同型別。
所有以兩個底線(__
)開頭的欄位名稱都被 GraphQL 保留。除了 __typename
,GraphQL 服務還會提供 __schema
和 __type
這些中繼欄位,讓你可以使用自省(introspection)系統。
指令(Directives)
前面提到,變數可以讓我們不用手動拼接查詢字串來產生動態查詢。用變數傳參數解決了很大一部分問題,但有時我們還需要根據變數動態改變查詢的結構。例如,有些 UI 元件有簡略和詳細兩種顯示方式,詳細模式會多查一些欄位。
我們可以為這種元件設計一個查詢:
// ... existing code ...
你可以試著把上面變數的 withFriends
改成 true
,看看結果有什麼不同。
這裡我們用到 GraphQL 的一個功能,叫做指令(directive)。特別是 可執行指令,可以加在欄位或片段上,由客戶端決定,伺服器可以根據指令改變查詢的執行方式。GraphQL 規範內建兩個指令,所有符合規範的伺服器都必須支援:
@include(if: Boolean)
只有在參數為true
時才包含這個欄位@skip(if: Boolean)
只有在參數為true
時才略過這個欄位
指令可以幫助你不用手動拼接查詢字串來增減欄位。伺服器實作也可以自訂新的指令來加入實驗性功能。
想了解如何定義可以用來標註 GraphQL schema 型別、欄位或參數的指令?請參考 Schemas and Types 頁面 了解如何定義與使用型別系統指令。
下一步
回顧一下我們學到的查詢重點:
- 讀取資料的 GraphQL 操作會從
query
根操作型別開始,依照選擇集一路往下到最底層的 Scalar 或 Enum 型別 - 欄位可以接受參數來改變回傳結果
- 操作可以用
query
、mutation
或subscription
關鍵字來標示型別 - 查詢操作可以省略操作型別關鍵字(只限查詢)
- 操作應該給予獨特名稱,讓請求更具表達力,也方便除錯
- 欄位別名可以讓你重新命名回傳 key、在同一查詢中多次包含同一欄位,並給不同參數
- 變數用
$
字元開頭,可以用來動態傳遞欄位參數 - 片段是可重複使用的欄位選擇集,可以在多個查詢中重複使用
- 可執行指令可以加在查詢上,讓伺服器在執行查詢時改變結果
- 所有符合規範的 GraphQL 伺服器都內建
@include
與@skip
這兩個指令
現在我們已經了解如何用查詢操作從 GraphQL 伺服器讀取資料,接下來要學習如何用 mutations 來改變資料並觸發副作用。