經過驗證後,GraphQL 查詢會由 GraphQL 伺服器執行,並傳回一個結果,其結構反映了所要求查詢的形狀,通常為 JSON。
沒有類型系統,GraphQL 無法執行查詢,讓我們使用一個範例類型系統來說明如何執行查詢。這是這些文章範例中使用的相同類型系統的一部分
type Query { human(id: ID!): Human}
type Human { name: String appearsIn: [Episode] starships: [Starship]}
enum Episode { NEWHOPE EMPIRE JEDI}
type Starship { name: String}
為了說明執行查詢時會發生什麼情況,讓我們使用一個範例來說明。
你可以將 GraphQL 查詢中的每個欄位視為前一個類型的函式或方法,傳回下一個類型。事實上,這正是 GraphQL 的運作方式。每個類型上的每個欄位都由稱為解析器的函式支援,而這個函式是由 GraphQL 伺服器開發人員提供的。執行欄位時,會呼叫對應的解析器來產生下一個值。
如果欄位產生一個純量值,例如字串或數字,則執行就會完成。但是,如果欄位產生一個物件值,則查詢將包含另一個適用於該物件的欄位選取。這會持續進行,直到到達純量值。GraphQL 查詢總是會在純量值結束。
在每個 GraphQL 伺服器的最上層,有一個類型代表進入 GraphQL API 的所有可能進入點,它通常稱為根類型或查詢類型。
在此範例中,我們的查詢類型提供了一個名為 human
的欄位,它接受引數 id
。這個欄位的解析器函式可能會存取資料庫,然後建構並傳回一個 Human
物件。
Query: { human(obj, args, context, info) { return context.db.loadHumanByID(args.id).then( userData => new Human(userData) ) }}
這個範例是用 JavaScript 編寫的,不過 GraphQL 伺服器可以用 許多不同的語言 建立。解析器函式會收到四個參數
obj
前一個物件,對於根查詢類型的欄位來說,通常不用到。args
GraphQL 查詢中提供給欄位的參數。context
提供給每個解析器的值,並包含重要的脈絡資訊,例如目前已登入的使用者,或對資料庫的存取權。info
包含與目前查詢相關的欄位特定資訊以及架構詳細資料的值,另請參閱 類型 GraphQLResolveInfo 以取得更多詳細資料。讓我們仔細看看這個解析器函式中發生了什麼事。
human(obj, args, context, info) { return context.db.loadHumanByID(args.id).then( userData => new Human(userData) )}
context
用於提供對資料庫的存取權,資料庫用來載入使用者的資料,方法是使用 GraphQL 查詢中作為參數提供的 id
。由於從資料庫載入資料是非同步作業,因此會傳回 Promise。在 JavaScript 中,Promise 用於處理非同步值,但許多語言中都有相同概念,通常稱為 未來、工作或 延遲。當資料庫傳回時,我們可以建構並傳回新的 Human
物件。
請注意,雖然解析器函數需要知道承諾,但 GraphQL 查詢並不需要。它只是希望 human
欄位傳回一些東西,然後它可以詢問 name
。在執行期間,GraphQL 會等待承諾、未來和任務完成後才繼續執行,並且會以最佳並行性執行。
現在,Human
物件可用,GraphQL 執行可以繼續執行其上要求的欄位。
Human: { name(obj, args, context, info) { return obj.name }}
GraphQL 伺服器由類型系統提供支援,用於決定下一步要執行什麼。甚至在 human
欄位傳回任何內容之前,GraphQL 就知道下一步將解析 Human
類型的欄位,因為類型系統告訴它 human
欄位將傳回 Human
。
在此情況下,解析名稱非常簡單。會呼叫名稱解析器函數,而 obj
參數是從前一個欄位傳回的 new Human
物件。在此情況下,我們預期 Human 物件具有 name
屬性,我們可以直接讀取和傳回。
事實上,許多 GraphQL 函式庫會讓您省略如此簡單的解析器,並且只會假設如果未提供欄位的解析器,則應讀取和傳回同名的屬性。
在解析 name
欄位時,可以同時解析 appearsIn
和 starships
欄位。appearsIn
欄位也可以有瑣碎的解析器,但讓我們仔細看看
Human: { appearsIn(obj) { return obj.appearsIn // returns [ 4, 5, 6 ] }}
請注意,我們的類型系統宣稱 appearsIn
將傳回具有已知值的列舉值,但此函數傳回數字!的確,如果我們查看結果,我們將看到傳回適當的列舉值。發生什麼事了?
這是標量強制轉換的範例。類型系統知道要預期什麼,並且會將解析器函式傳回的值轉換成符合 API 契約的內容。在此情況下,我們的伺服器上可能定義了一個使用數字(例如 4
、5
和 6
)的列舉,但在 GraphQL 類型系統中將其表示為列舉值。
我們已經看過一點,當欄位傳回一串具有上述 appearsIn
欄位的項目時會發生什麼事。它傳回一串列舉值,而且由於這是類型系統所預期的,因此串列中的每個項目都強制轉換為適當的列舉值。當解析 starships
欄位時會發生什麼事?
Human: { starships(obj, args, context, info) { return obj.starshipIDs.map( id => context.db.loadStarshipByID(id).then( shipData => new Starship(shipData) ) ) }}
此欄位的解析器不僅傳回 Promise,它傳回一串 Promise。Human
物件有一串他們駕駛的 Starships
的 id,但我們需要載入所有這些 id 才能取得真正的 Starship 物件。
GraphQL 會同時等待所有這些 Promise 繼續進行,當取得一串物件時,它會再次同時繼續載入這些項目中每個項目的 name
欄位。
當每個欄位解析完畢後,結果值會放入一個鍵值對應中,其中欄位名稱(或別名)為鍵,解析值為值。這會從查詢的最底層欄位一直持續到根查詢類型上的原始欄位。這些會共同產生一個結構,反映原始查詢,然後可以將其傳送(通常以 JSON 格式)給請求的客戶端。
讓我們最後再看一次原始查詢,看看這些解析函式如何產生結果