1. 程式人生 > >GraphQL簡單學習-執行原理

GraphQL簡單學習-執行原理

浪費了“黃金五年”的Java程式設計師,還有救嗎? >>>   

一、簡介

可以參考一下前兩天寫的文章:https://my.oschina.net/hanchao/blog/3014079

二、Execution

參考文章:https://graphql.org/learn/execution/

參考文章:http://graphql.cn/learn/execution/

這篇文件解釋了GraphQL是如何根據定義的schema來完成資料的組裝和聚合的,應該算是整個GraphQL架構中最核心的設計之一了,值得了解。

在必要的資料校驗環境後,GraphQL服務端會根據每一個query的實際要求來剪裁恰如其分的資料結構以進行響應,通常來說是JSON格式。

GraphQL非常依賴其型別模型(type system),我們來看一個實際的例子來演示如何執行一個query。這個例子和文件中其他部分是一致的:

type Query {
  human(id: ID!): Human
}

type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  name: String
}

為了瞭解query執行的細節,我們來看一個例子:

// 請求
{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}

// 響應
{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}

你可以將在GraphQL查詢中每一個欄位(field)看做是前一種型別的函式或方法,它返回下一種型別。事實上,這就是GraphQL的工作原理

在GraphQL服務端,每一個型別的每一個欄位背後都是靠一個被稱為 resolver 的函式來提供資料(提供支援)的。每當一個欄位被要求返回,與其對應的 resolver 函式就會被執行,併產生下一個值(進入新一輪resolver 執行)。

 

如果發現欄位返回的是一個標量值,如字串或數字,此時執行就算告一段落了。

然而如果該欄位執行後返回的是一個物件值,那麼執行器會繼續試圖獲取該物件值包含的欄位,一直到最終得到一個標量值為止。

Root fields & resolvers 

每個GraphQL服務的最頂層型別是一個包含一切的統一API入口型別,通常我們稱之為 Roottype 或 Querytype 

在我們的例子中, Querytype 提供了一個欄位叫 human ,它接受一個引數 id 。這個欄位對應的 resolver 函式通過操作資料庫並構建和返回 human 物件。

Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

這個例子是用JavaScript寫的,但GQL服務端可以用 多種語言 來實現。 resolver 函式接受4個引數:

  • obj: 前一個物件(譯:觸發該 resolver 的欄位所在的物件),這個引數對最頂層欄位沒有意義(譯:當然啊,不然嘞~)
  • args: 來自GraphQL query
  • context: 提供所有 resolver 依賴的資源,例如資料庫連線物件,當前登入的使用者資訊等
  • info: 包含與當前查詢相關的特定於欄位的資訊的值,以及模式細節,可以參考 graphqlobjecttype

Asynchronous resolvers

我們來近距離看一下這個 resolver 函式的細節:

human(obj, args, context, info) {
  return context.db.loadHumanByID(args.id).then(
    userData => new Human(userData)
  )
}

context 引數中包含了資料庫連線物件,用於資料查詢來得到GraphQL query中要求的 id 資料。查詢資料庫是一個非同步呼叫,所以返回一個 promise 。 promise 是javascript中的非同步呼叫概念,不過其他很多語言也有對應的實現方式,通常被稱之為 futures , tasks 或者 Deferred 。當資料庫操作返回後,我們就可以構建和返回一個新的 Human物件啦。

需要注意的是儘管 resolver 函式返回的是 promise ,但GraphQL query並不是非同步的,它會期望 human 攜帶了所有要求返回的資料。在執行過程中,GQL會一直等到 PromiseFutures 或 Tasks 完結後才會繼續並最大化保持併發度(譯:這一點很重要)。

Trivial resolvers

現在我們已經得到了一個 Human 物件,接下來GraphQL執行器將繼續處理其下的欄位。

Human: {
  name(obj, args, context, info) {
    return obj.name
  }
}

GraphQL服務依靠型別系統來決定如何繼續執行下去。甚至是在 human 返回任何結果之前,GraphQL就可以根據型別系統要求提供的 human 型別宣告得到下一步應該處理的欄位有哪些。

在這個例子裡 name 欄位的處理是非常簡單明瞭的。傳入 name resolver 函式的 obj 引數就是前一步返回的那個 new Human 物件。例子中我們期望得到的 human 物件包含name 欄位,已經如願以償。

事實上,很多GraphQL類庫都不需要你提供這種簡單的 resolver ,它們會預設自動從 obj 中讀取並返回對應名字的屬性(譯:預設解析器規則)。

Scalar coercion

當 name 欄位被處理過後, appearsIn 和 starships 欄位會被同時處理。 appearsIn 欄位也有一個 trivial resolver ,我們來仔細看一下:

Human: {
  appearsIn(obj) {
    return obj.appearsIn // returns [ 4, 5, 6 ]
  }
}

注意,我們的型別系統宣告 appearsIn 將返回一個列舉型別,然而這個函式卻返回的是number陣列!實際上,如果我們檢視結果,我們將看到相應的Enum值被歸還。發生了什麼?

這就是一個 Scalar coercion 的例子。型別系統知道應該返回什麼,並將解析器返回的資料轉換成了API宣告要求的型別。在這個例子中,在服務的其他位置應該存在一個列舉型別的定義來標識 4,5,6 對應的列舉值。

List resolvers

通過 appearsIn ,我們已經看到了當一個欄位需要一個返回多條資料時的細節。它返回了一個列舉值陣列,然後型別系統將每個值轉換成了對應的列舉值。那 starships 欄位解析的細節有是什麼呢?

Human: {
  starships(obj, args, context, info) {
    return obj.starshipIDs.map(
      id => context.db.loadStarshipByID(id).then(
        shipData => new Starship(shipData)
      )
    )
  }
}

這個欄位的 resolver 不只是返回一個 promise ,它返回了一個 promise 陣列。 human 物件擁有一個 starships 的 id 陣列,但我們需要載入所有這些id 關聯的 starship 物件。

GraphQL會等待所有的 promise 併發的完成後才會繼續,當所有 starship 物件都得到後,GQL會繼續併發的嘗試獲取這些物件的 name 欄位。

Producing the result

當所有欄位都處理完畢後,結果值構建成一個從葉子節點到根節點全鏈路的鍵值對對映,鍵為欄位名,值為 resolver 返回的結果,最終按照請求的結構返回給客戶端對應的資料結構(JSON結構)。

 

 

參考博文:

https://blog.kazaff.me/2018/03/25/%E5%86%8D