[譯]GraphQL如何把查詢轉換為響應(How GraphQL turns a query into a response)

isNeilLin發表於2019-03-04

原文連結: How GraphQL turns a query into a response

在這篇文章中,我將回答一個簡單的問題,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”訂閱者的nameemail。以下是響應的樣子:

{
  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伺服器獲取的資料模型。它定義了允許客戶端進行哪些查詢,可以從伺服器獲取什麼型別的資料,以及這些型別之間的關係。例如:

schema
具有三種型別的簡單GraphQL模式:Author、POST和Query

在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可以不寫

這個模式非常簡單:它宣告應用程式有三種型別: -  AuthorPOSTQuery。每個查詢都必須從它的一個欄位開始:getAuthorgetPostsByTitle。你可以把它們看作是REST端點,除了更強大之外。

AuthorPost 相互引用。你可以通過 Authorposts欄位獲取 Post,也可以通過 Postauthor欄位從獲取 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 返回的型別上是否有nameposts欄位?
  • ...諸如此類

作為一個應用程式開發人員,您不需要擔心這個部分,因為GraphQL伺服器會自動完成。這與大多數RESTfulAPI形成了對比,在這種情況下,需要由開發人員來確保所有引數都是有效的。

Step 3: 執行

如果通過驗證,GraphQL伺服器將執行查詢。

每個GraphQL查詢都具有樹的形狀,也就是說,它從不是迴圈的。執行從Query的根開始。首先,執行器呼叫頂層欄位的解析函式-在本例中,只是 getAuthor。它等待直到所有這些解析函式返回一個值,然後以級聯的方式在下一級繼續。如果一個解析函式返回一個promise,執行者將等待該promiseresolved。

這是對執行流的一段描述。我認為,當以不同的方式展示事物時,它們總是更容易理解,所以我製作了一張圖表,一張表格,甚至一段視訊,一步步地帶你去看。

圖形式的執行流程:

exexution
執行從最上面開始。在同一級別上的解析函式是併發執行的

表形式的執行流程:

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的引數idgetAuthor 解析函式將執行並返回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上nameposts欄位的解析函式,因為這些欄位是查詢中請求的欄位。nameposts欄位的解析函式並行執行。

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之後才執行下一級的解析函式

因為查詢請求了每個帖子的titleauthor欄位,所以GraphQL並行執行四個解析函式:每個帖子的titleauthor

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執行高效能的現代應用程式,我們可以幫助您!讓我們知道

Apollo Engine
Apollo Engine的執行概況:查詢服務時間和請求率/錯誤率圖表的熱力圖。

相關文章