在這篇文章中,我將回答一個簡單的問題,GraphQL如何把查詢轉換為響應?
如果你對GraphQL還不熟悉,那麼在閱讀之前,先了解一下“How do I GraphQL?”的三分鐘介紹。這樣你就能從這篇文章中得到更多。
我們這篇文章中將會介紹以下內容:
- GraphQL queries – 查詢
- Schema and resolve functions – 模式和解析函式
- GraphQL execution — step by step – 逐步執行
準備好了嗎?讓我們開始吧!
GraphQL queries
GraphQL查詢結構非常簡單,易於理解。請看下面的例子:
{
subscribers(publication: "apollo-stack"){
name
email
}
}
複製程式碼
如果我們為Building Apollo構建了一個API,顯而易見這個查詢將會返回所有訂閱了“apollo-stack”訂閱者的name
和email
。以下是響應的樣子:
{
subscribers: [
{ name: "Jane Doe", email: "jane@doe.com" },
{ name: "John Doe", email: "john@doe.com" },
...
]
}
複製程式碼
注意響應的結構與查詢的結構幾乎相同。GraphQL的客戶端非常簡單,它實際上是自解釋的!
但是服務端呢?會更復雜嗎?
事實證明,GraphQL服務端也相當簡單。在閱讀完這篇文章之後,您將清楚地瞭解GraphQL伺服器內部發生了什麼,並準備好構建自己的伺服器。
Schema and Resolve Functions
每個GraphQL伺服器都有兩個核心部分來決定它的工作方式:schema(模式) 和 resolve functions(解析函式)。
模式:模式是可以通過GraphQL伺服器獲取的資料模型。它定義了允許客戶端進行哪些查詢,可以從伺服器獲取什麼型別的資料,以及這些型別之間的關係。例如:
在GraphQL模式語法中,如下所示:
type Author {
id: Int
name: String
posts: [Post]
}
type Post {
id: Int
title: String
text: String
author: Author
}
type Query {
getAuthor(id: Int): Author
getPostsByTitle(titleContains: String): [Post]
}
schema {
query: Query
}
複製程式碼
譯者注:在Apollo-Server2.0中,最後一節schema可以不寫
這個模式非常簡單:它宣告應用程式有三種型別: - Author、POST 和 Query。每個查詢都必須從它的一個欄位開始:getAuthor 或 getPostsByTitle。你可以把它們看作是REST端點,除了更強大之外。
Author 和 Post 相互引用。你可以通過 Author 的posts
欄位獲取 Post,也可以通過 Post 的author
欄位從獲取 Author。
模式告訴伺服器允許客戶端進行哪些查詢,以及不同型別之間的關係,但是其中有一個關鍵資訊是不包含的:每種型別的資料來自哪裡!
這就是解析函式的用途。
Resolve Functions
解析功能有點像路由。它們指定模式中的型別和欄位如何連線到各種後端,解決“如何為 Author 獲取資料?”和“我需要用什麼引數呼叫哪個後端才能獲得 POST 的資料?”這樣的問題。
GraphQL解析函式可以包含任意程式碼,這意味著GraphQL伺服器可以與任何型別的後端,甚至其他GraphQL伺服器對話。例如,Author 型別可以儲存在SQL資料庫中,而 POST 可以儲存在MongoDB中,甚至可以由微服務處理。
也許GraphQL最大的特點是它對客戶端隱藏了所有後端複雜性。不管您的應用程式使用了多少後端,客戶端只會看到一個帶有應用程式簡單的、自文件化API的GraphQL端點。
下面是兩個解析函式的例子:
getAuthor(_, args){
return sql.raw(`SELECT * FROM authors WHERE id = %s`, args.id);
}
posts(author){
return request(`https://api.blog.io/by_author/${author.id}`);
}
複製程式碼
當然,您不會將查詢或url直接寫入一個解析函式中,而是將其放在一個單獨的模組中。但你已經明白瞭解析函式的使用。
Query execution — step by step
好了,現在您已經瞭解了模式和解析函式,讓我們來看看實際查詢的執行情況。
附帶說明:下面的程式碼是GraphQL-JS的程式碼,它是GraphQL的JavaScript參考實現,但是在我所知道的所有GraphQL伺服器中,執行模型是相同的。
在本節的末尾,您將瞭解GraphQL伺服器如何使用模式和解析函式一起執行查詢並生成所需的結果。
下面是一個與前面介紹的模式對應的查詢。它獲取一個作者的姓名、該作者的所有帖子以及每個帖子的作者的姓名。
{
getAuthor(id: 5){
name
posts {
title
author {
name # this will be the same as the name above
}
}
}
}
複製程式碼
附帶說明:如果仔細觀察,您會注意到這個查詢兩次獲取同一個作者的名稱。我在這裡這樣做只是為了說明GraphQL,同時保持模式儘可能簡單。
以下是伺服器響應查詢的三個關鍵步驟:
1、解析
2、驗證
3、執行
Step 1: 解析查詢
首先,伺服器解析字串並將其轉換為AST(抽象語法樹)。如果有任何語法錯誤,伺服器將停止執行並將語法錯誤返回給客戶端。
Step 2: 驗證
一個查詢在語法上可以是正確的,但仍然沒有任何意義,就像下面的英語句子在語法上是正確的,但是沒有任何意義:“The sand left through the idea”。
驗證階段確保在開始執行之前給定模式查詢是有效的。它檢查如下:
- getAuthor 是查詢型別的欄位嗎?
- getAuthor 是否接受名為
id
的引數? - getAuthor 返回的型別上是否有
name
和posts
欄位? - …諸如此類
作為一個應用程式開發人員,您不需要擔心這個部分,因為GraphQL伺服器會自動完成。這與大多數RESTfulAPI形成了對比,在這種情況下,需要由開發人員來確保所有引數都是有效的。
Step 3: 執行
如果通過驗證,GraphQL伺服器將執行查詢。
每個GraphQL查詢都具有樹的形狀,也就是說,它從不是迴圈的。執行從Query
的根開始。首先,執行器呼叫頂層欄位的解析函式-在本例中,只是 getAuthor。它等待直到所有這些解析函式返回一個值,然後以級聯的方式在下一級繼續。如果一個解析函式返回一個promise
,執行者將等待該promise
resolved。
這是對執行流的一段描述。我認為,當以不同的方式展示事物時,它們總是更容易理解,所以我製作了一張圖表,一張表格,甚至一段視訊,一步步地帶你去看。
圖形式的執行流程:
表形式的執行流程:
3.1: run Query.getAuthor
3.2: run Author.name and Author.posts (for Author returned in 3.1)
3.3: run Post.title and Post.author (for each Post returned in 3.2)
3.4: run Author.name (for each Author returned in 3.3)
複製程式碼
為了方便起見,這還是上面的查詢:
{
getAuthor(id: 5){
name
posts {
title
author {
name # this will be the same as the name above
}
}
}
}
複製程式碼
在這個查詢中,只有一個根欄位 getAuthor 和一個值為5的引數id
。getAuthor 解析函式將執行並返回Promise。
getAuthor(_, { id }){
return DB.Authors.findOne(id);
}
// let`s assume this returns a promise that then resolves to the
// following object from the database:
{ id: 5, name: "John Doe" }
複製程式碼
當資料庫呼叫返回時,Promise將被resolved。一旦發生這種情況,GraphQL伺服器將獲取此解析函式的返回值-在本例中為一個物件-並將其傳遞給Author上name
和posts
欄位的解析函式,因為這些欄位是查詢中請求的欄位。name
和posts
欄位的解析函式並行執行。
name(author){
return author.name;
}
posts(author){
return DB.Posts.getByAuthorId(author.id);
}
複製程式碼
name
解析函式非常簡單:它只返回剛剛從 getAuthor 解析函式傳遞下來的Author物件的name屬性。
posts
解析函式呼叫資料庫並返回POST物件列表:
// list returned by DB.Posts.getByAuthorId(5)
[{
id: 1,
title: "Hello World",
text: "I am here",
author_id: 5
},{
id: 2,
title: "Why am I still up at midnight writing this post?",
text: "GraphQL`s query language is incredibly easy to ...",
author_id: 5
}]
複製程式碼
注意: GraphQL-JS等待列表中所有的Promise被resolved或者rejected之後才執行下一級的解析函式
因為查詢請求了每個帖子的title
和author
欄位,所以GraphQL並行執行四個解析函式:每個帖子的title
和author
。
title
解析函式像name
一樣是微不足道的,author
解析函式與 getAuthor 的函式相同,只不過它在POST上使用author_id
欄位,而 getAuthor 函式使用id
引數:
author(post){
return DB.Authors.findOne(post.author_id);
}
複製程式碼
最後,GraphQL執行器再一次呼叫Author的name
解析函式,這一次使用POSTS的author
解析函式返回的Author物件。它執行了兩次—— 每個帖子執行一次。
到這裡執行部分已經結束了!剩下要做的就是將結果傳遞到查詢的根目錄,並返回結果:
{
data: {
getAuthor: {
name: "John Doe",
posts: [
{
title: "Hello World",
author: {
name: "John Doe"
}
},{
title: "Why am I still up at midnight writing this post?",
author: {
name: "John Doe"
}
}
]
}
}
}
複製程式碼
注意:這個例子稍微簡化了一些。真正的生產GraphQL伺服器將使用批處理和快取來減少對後端的請求數量,並避免產生冗餘的請求,比如獲取同一作者兩次。但這是另一篇文章的主題!
結語
如你所見,一旦你深入到它,GraphQL是非常容易理解的!我認為GraphQL在解決諸如聯表、過濾、引數驗證、文件等傳統RESTfulAPI中很難解決的問題上,是非常出色的。
當然,GraphQL比我在這裡寫的要多得多,但這是以後文章的主題!
如果這讓您對自己嘗試GraphQL感興趣,您應該檢視我們的GraphQL server tutorial,或者閱讀有關 using GraphQL on the client together with React + Redux.的相關內容。
2018年更新:理解使用Apollo Engine執行GraphQL
自從Jonas撰寫這篇文章以來,我們還構建了一個名為Apollo Engine的服務,通過提供以下功能幫助開發人員瞭解和監視其GraphQL伺服器中發生的事情:
如果您有興趣看到您的GraphQL查詢在實際應用中的執行,您可以在這裡登入並檢測您的伺服器。如果您有興趣支援使用GraphQL執行高效能的現代應用程式,我們可以幫助您!讓我們知道。