iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 3
3

header

本篇我會從 GraphQL 的精神: 查詢語言 (query language) 開始說起。

我自己剛學習時,常因分不清楚 schema 與 query 的 syntax 使用方式與時機而卡住,比如:

  • field 與 type 到底差別在哪裡?
  • Object Type 使用時機為何?
  • query 時哪些 field 可以展開哪些不行 ?

(問題解答附在文章末)

後來發現諸如此類問題的癥結點在於沒有釐清 query (資料查詢) 與 schema (架構設計) 的不同:一個是 client 送來的 request , 一個是 server 維護的架構,完全是兩個世界的東西!
以餐廳來比喻, 就像是客人 (client side)菜單 (Schema) 上的資訊來 **點餐 (發送 request)**一樣!

Query 是 client side 是符合 schema 規定的查詢語言格式 ;
Schema 是 server side 定義整體資料結構格式。

(可見 GraphQL 官方教學 右側的目錄也是先教 Query and Mutations 再教 Schemas and Types)

但為了讓大家在還沒寫出自己的 schema 之前可以練習 Query,請先打開我在第一天分享的連結 (傳送門: My GraphQL Playground Example)。

如果你覺得 GraphQL Playground 的 UI 太複雜不好看,可以複製 GraphQL Endpoint (也就是上面的 API 網址: https://z5r11749r7.lp.gql.zone/graphql) 然後到下載 GraphiQL 桌面程式 使用,效果如圖


基礎 Query

點開範例後,這是一張 query syntax 的說明圖:

https://imgur.com/jz0e8ea

接下來我會用 3 個範例來讓大家了解這張圖所表達的概念!

1. 型別 (type) 釐清

請大家在範例中 (左側)輸入:

query {
  # 目前使用者
  me {
    id
    name
  }
}

右側可以得到

{
  "data": {
    "me": {
      "id": 1,
      "name": "Fong"
    }
  }
}

如圖:
https://imgur.com/JXijZ1T

接下來打開旁邊的 Schema 標籤會跑出 Documentation,長得像這樣:

https://imgur.com/tn1KcUV

兩者搭配之下可以觀察到:

  1. 在 Schema 中 name field 的資料型別 (type) 為 String,與取得的資料 ("Fong") 型別相符合

    這種擁有實際值的 field 的型別 (type),我們稱為 Scalar Type (基礎型別)。
    其他基礎型別還包括 Int, Float, String, Boolean, ID

  2. me 這個 field 的資料型別 (type) 為 UserUser 是一種自定義的 Object Type,展開後可得到一系列的 field

    這種 自定義能展開的型別 (type) ,我們稱為 Object Type

  3. # 是 query 中的備註,不會被 GraphQL Server 處理

到這裡大家的心中一樣可能會浮現一個疑惑:到底 UserObject Type 以及 me 是什麼關係 ?

讓我們以 JAVA 物件導向的例子說明:

class Person { /* Defining your fields and methods */}
Person m = new Person();

我們的 Schema 與以上程式相比, Object Type 如同 Class 一樣是一種廣泛的概念,用於表達一個類型的超集,不會直接拿來使用。 User 就如同 Class 宣告的 Person 一樣,擁有著特定的資料結構,會使用來定義物件。而物件 m 跟 field me 的意思很像,都可以讓我們直接存取結構裡的資料!

Person 是一種 Class,而 m 是一個 Person class 的物件 ;
User 是一種 Object Type,而 me 是一個 User type 的 field 。

另外,GraphQL 的 Operation Type 預設為 query ,所以使用 query 的話可以省略第一行直接下 {

2. 遞迴 (recursive) 取值

如同一個物件導向程式裡的物件定義的 field 可以是實際值或是 reference 到不同 class 的物件; GraphQL 的 Object Type 也可以透過 field 來指向相同或不同的 Object Type。回到 GraphQL Playground ,再次輸入:

query {
  # 目前使用者
  me {
    id
    name
+   # 目前使用者的貼文
+   posts {
+     id
+     title
+   }
  }
}

可以得到

{
  "data": {
    "me": {
      "id": "1",
      "name": "Fong",
      "posts": [
        {
          "id": "1",
          "title": "Hello World!!"
        },
        {
          "id": "4",
          "title": "Love U Too"
        }
      ]
    }
  }
}

如圖:
https://imgur.com/3gP35vN

請再打開 Schema 並點選 me -> User.post -> Post:
https://imgur.com/8DEgC1k

兩者搭配之下可以觀察到:

  1. posts 這個 field 的 type 為 [Post] ,代表得到的資料格式為一個 PostArray

    如果展開的是 Array type 的 field ,那就會以 json array 的方式([])呈現

  2. query 到 Object Type 的 field 時一定要展開並選取 fields

    GraphQL Server 會依據 query 選取的 field 給予 實際值,所以如果 Object Type 不展開 GraphQL Server 會給不出實際值。

    query 結構最末端的 field 一定要是 Scalar Type

  3. Object Type 使用 field 互相連接!

在 schema 中可發現 User type 的 friends field 是 [User] type ,而 Post type 的 field author 也是 User type。

所以你可以做出這樣的操作:「取得我所有朋友的朋友的貼文的作者的生日」XD,這也是 GraphQL 有趣且強大的地方,一次拿到所有資料!

PS 對安全性有警覺性的朋友一定有發現「如果 client 端傳來無限深的 query 搞爆 server 怎麼辦 ?」,別擔心,之後會提到一些解決方法!

看完以上兩個例子,我們可以得到一個重要的結論:

GraphQL query 基本上就是選取 Object 的 field 來獲取資料

3. 自己來動手

請寫出正確的 query 取得 「我的朋友們的朋友們的貼文裡,那些按讚的人們的名字」,
並告訴我「我第一個朋友的第一個朋友的第一則貼文的第一個按讚的人是誰?」

解答: 點開附圖


Query 執行的背後機制

如果你在以上的過程中輸錯過字或是沒展開 Object Type 的 field 的話, GraphQL Playground 的就會亮起紅燈 (紅色底線) 以阻止你送出錯誤的 Query。若是你忽視警告硬要闖紅燈送出 Query 的話, 就會得到 Error,如圖:

  1. me 後面沒有打上 {: https://imgur.com/3JveHg7
  2. 打錯 name: https://imgur.com/k1MuMaG
    Error Message: Cannot query field \"nmae\" on type \"User\". Did you mean \"name\"?
  3. 沒有展開 Object Type: https://imgur.com/67Lpq58
    Error Message: Field \"friends\" of type \"[User]\" must have a selection of subfields. Did you mean \"friends { ... }\"?

而到底 GraphQL 是怎麼做到的呢?我來簡短講解一下背後的原理:
GraphQL Server 收到 Query 後

  1. 解析 (Parse) 這段 query 成 AST (abstract syntax tree ,一種正規化的資料結構)

    如果有 syntax error (少加括號或 keyword 打錯),立即回傳 Error,如錯誤 1 。

  2. 第一步通過後,進入 驗證(Validation) 階段

    此時 GraphQL 會詳細檢查 field 的細項,如以上提到的兩個範例,如果有錯立即回傳 Error,如錯誤 2 跟 3

  3. 前面兩步都通過後,才會進入 執行 (Execution) 階段。

    工作會由 GraphQL Server 轉交給真正的 Server 去處理資料 (做計算或從 DB 拿資料)。

    而回傳時 GraphQL 會將資料轉換成 Schema 中對應的格式,如果無法成功轉換 (如 StringInt),會立即回傳 Error 。

    當然,也有可能發生問題如權限不足、資料取得失敗或其他商業邏輯引起的錯誤等等,此時也會回傳 Error

延伸閱讀: GraphQL explained
延伸閱讀: GraphQL Learn Validation & Execution


所以 Query 到底是用什麼黑科技送出去?

在這邊強調,一般來說

GraphQL 的 Query 及之後會提到的 Mutation 都是使用 POST 送出 !

包含以上的 IDE 背後也都是使用 POST !有人會說,為什麼 Query 不像 RESTful API 使用 GET 呢 ? 原因很簡單:因為 POST 才能自帶 body 啊!
以上 query 的內容其實就是 POST 的 body。

大家可以在 GraphQL Playground 送出 Query 前打開瀏覽器的開發者工具 (或對畫面點右鍵選 Inspect),
進入到 Network 的 tab 選 All 後,送出 Query 看第二筆紀錄,如圖:
https://imgur.com/F0HW0Pu

可以看到他的 payload 其實就是一個 JSON,所以如果像前端要用 fetch 的話會像是這樣 (可直接在開發者工具的 console 輸入):

  fetch('https://z5r11749r7.lp.gql.zone/graphql', {
    body: JSON.stringify({ query: "{\n  me {\n    id\n    name\n  }\n}\n" }),
    headers: {
      'content-type': 'application/json' // 這一欄一定要設定!
    },
    method: 'POST',
  })
  .then(response => response.json()) // 輸出成 json
  .then(data => console.log(data))

結果如圖:
https://imgur.com/kX1h1VO

PS 當然 query 也可以使用 GET 送出,但真的很難用也浪費了 GraphQL 優雅的設計,可參考 GraphQL Learn: Serving Over Http


開頭問題解答:

  • field 與 type 到底差別在哪裡?
    → field 是組成 Object Type 的欄位, type 為 field 展現的資料格式。
  • Object Type 使用時機為何?
    → Object Type 是一種概念,不會真的使用,而是透過定義自訂的 Object Type 資料結構來使用。
  • query 時哪些 field 可以展開哪些不行 ?
    → 只有當 field 的 type 為 Object 時才能展開 (代表裡面才有層狀的資料結構可讀取)

看完今天的文章,想必各位都已經對於 GraphQL 的 query 有了基本的認知,眾所期待的 GraphQL Server 實作將會在明天推出!手癢想打 code 的朋友們千萬別錯過!

這篇以後會比較難,如果有任何不懂的地方歡迎底下留言,我會盡力幫忙解惑!


上一篇
GraphQL 入門:生態圈 X 工具 X 選擇
下一篇
GraphQL 入門: Server Setup X NodeJS X Apollo (寫程式囉!)
系列文
Think in GraphQL30

尚未有邦友留言

立即登入留言