跳至主要内容

查詢(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 型別,代表有限選項之一(這裡是長度單位,可以是 METERFOOT)。GraphQL 內建一組預設型別,但 GraphQL 伺服器也可以宣告自訂型別,只要能序列化成你的傳輸格式即可。

操作型別與名稱(Operation type and name)

在上面的範例中,我們使用了一種簡寫語法,省略了在操作選擇集前面的 query 關鍵字。除了可以明確指定 操作型別 之外,我們也可以為操作加上一個獨特的 操作名稱,這在正式環境的應用程式中特別有用,因為這樣有助於除錯與追蹤。

以下是一個同時包含 query 關鍵字(操作型別)與 HeroNameAndFriends(操作名稱)的範例:

# { "graphiql": true }
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}

操作型別可以是 querymutationsubscription,用來描述你想執行的操作類別。這個關鍵字在查詢時可以省略(只限查詢),但在變更與訂閱時則是必須的。如果你想為操作命名,也必須明確指定操作型別。

操作名稱是你為操作自訂的名稱,建議取一個有意義的名字。當你在一份文件中傳送多個操作時,操作名稱是必須的;即使只傳送一個操作,也建議加上名稱,因為這對除錯與伺服器端日誌紀錄都很有幫助。當出現錯誤(不論是在網路日誌還是 GraphQL 伺服器的日誌中),你可以更容易地根據名稱在程式碼中找到對應的查詢,而不用去猜查詢內容。

別名(Aliases)

如果你很細心,可能已經注意到,因為回傳結果的欄位名稱會和查詢中的欄位名稱相同,但不包含參數,所以你無法直接用不同參數查詢同一個欄位。這時你就需要別名——它可以讓你把欄位的回傳結果重新命名成你想要的名稱。

# { "graphiql": true }
query {
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}

在上面的例子中,兩個 hero 欄位本來會衝突,但因為我們可以用別名把它們改成不同的名稱,就能在同一次請求中取得兩個結果。

變數(Variables)

到目前為止,我們都是直接在查詢字串中寫死所有參數。但在大多數應用程式中,欄位的參數通常是動態的。例如,可能有一個下拉選單讓你選擇想看的星際大戰集數,或是一個搜尋欄位,或是一組篩選條件。

如果把這些動態參數直接寫在查詢字串裡,客戶端程式就必須在執行時動態操作查詢字串,並序列化成 GraphQL 特有的格式。為了解決這個問題,GraphQL 提供了第一級的變數支援,可以把動態值從查詢中抽離,另外用一個物件傳遞。這些值就叫做變數

當我們開始使用變數時,需要做三件事:

  1. 把查詢裡的靜態值換成 $變數名稱
  2. 在查詢中宣告 $變數名稱 為可接受的變數
  3. 在另外一個(通常是 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 參數可能是 HumanDroid。在直接選擇時,你只能查 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 型別
  • 欄位可以接受參數來改變回傳結果
  • 操作可以用 querymutationsubscription 關鍵字來標示型別
  • 查詢操作可以省略操作型別關鍵字(只限查詢)
  • 操作應該給予獨特名稱,讓請求更具表達力,也方便除錯
  • 欄位別名可以讓你重新命名回傳 key、在同一查詢中多次包含同一欄位,並給不同參數
  • 變數用 $ 字元開頭,可以用來動態傳遞欄位參數
  • 片段是可重複使用的欄位選擇集,可以在多個查詢中重複使用
  • 可執行指令可以加在查詢上,讓伺服器在執行查詢時改變結果
  • 所有符合規範的 GraphQL 伺服器都內建 @include@skip 這兩個指令

現在我們已經了解如何用查詢操作從 GraphQL 伺服器讀取資料,接下來要學習如何用 mutations 來改變資料並觸發副作用。