iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 5
1

header

架好 local project 後,我們就來聊聊組成 GraphQL Server 的三大靈魂: Schema 、 Resolver 以及 Web Server!
Web Server 在昨天的 project 中很明顯的被 ApolloServer 解決了,今天要處理 Schema 與 Resolver 兩個與 API 設計相關的概念,並且開始實作簡單社交軟體的第一步:取得 user 資料。

PS 因為網路資源多以英文為主,所以在專有名詞上我會盡量以英文為主以利各位未來學習,但很多名詞其實都沒有一個確切的英文名稱,所以我會試著選用概念相近的單字,若有不當的用法還肯請指正。


GrphALQ API 兩大基石: Schema + Resolver

要組成一個 GraphQL API 需要一個架構跟實作架構的行為:

  1. 架構: Schema 定義 GraphQL API 的輪廓及規範
  2. 行為: Resolver 負責資料取得的實作

來打個比方,今天你進去麥當勞會先抬頭注意牆上的 菜單,這個菜單就如同 GraphQL 的 Schema,一一列出關於「你可以點的食物 (查詢的資料)」、「餐點與餐點的套餐搭配 (資料間的關聯)」、「套餐的內容物 (資料的架構)」等的資訊。

一個 GraphQL Schema 是由很多的 type 、 type 之間的關聯以及資料結構所組成

想好餐點後、點完餐,後面的廚房就開使針對你的餐點 料理出你要的食物 (拿出你要的資料),也就是 Resolvers 在做的事情。

Resolver 的工作就是基於 Schema 的設計來完成資料取得 & 計算的任務

簡而言之,Schema 是由 GraphQL 的 type 所組成的 Type system ,Resolver 則是對應 Schema 的需求來取得與計算所需的資料,也可當作是 GraphQL API 的實作。

小問題 1:如果 Schema 是菜單、 Resolver 是廚房,那點餐的你是?
小問題 2: 所以 business logic 也是 Resolver 的守備範圍囉?
答案在文章末

在 Schema 揮灑你的設計細胞

Schema 就是你的 API 的模樣,也是你實現你的資料模型的地方,以下是 Scheam Definition 示意圖:

https://imgur.com/O2lGGen

有了一個基本概念,我們就來介紹 Schema 中最重要的概念:

Types & Fields

再複習一次,什麼是 Type 呢?中文翻譯就是「型別」,也就是資料的展現形式。
Type 可以說是建構 GraphQL Schema 的基石,其中最基本的就是 object type,可以回想第一天的範例:

type User {
  id: Int
  name: String
  age: Int
}

type Query {
  me: User
}

上面的 User 就是一個自定義的 Object type,裡面有許多自定義的 field (欄位),每個 field 後面都要加上自己的 type 。
這個 type 可以是一個 Object type ,也可以是實際的值如 Integer, String, Boolean 等等,而這些實際的值的型別我們稱為 Scalar Type

五種預設 Scalar Type

Scalar Type 與 Object Type 不同的點在於 Scalar Type 會存取實際資料,預設的 Scalar Type 有以下五種:

  1. Int: 整數
  2. Float: 浮點數
  3. String: UTF‐8 字串
  4. Boolean: True or False.
  5. ID: 識別碼

需要特別注意 ID 方面,只是語意的表達並未實際做什麼檢查,所以傳入 Int 或 String 都可以過。不過實作時通常是傳入 uuid string。

那如果 Resolver 處理出的資料不符合 Type 的要求呢?

很簡單,若 GraphQL 檢查出資料與 Schema 裡某 field 的 Type 不合的話就會直接吐出 Error ,幫你嚴格把關、只接受符合的資料型別!
舉例來說,若 field Type 為 Int 但 Resolver 卻餵給他 "ABC", Server 就會吐出 "message": "Int cannot represent non-integer value: \"string\"", 的錯誤。
不過需要特別注意,Null是可以被接受的。不管是什麼你後面接什麼 Type 預設上都是 Nullable ,因此即使你要 Int 它給你 Null 在 GraphQL 處理時是不會報錯的。

另外也可以自己實作出自定義的 Scalar Type 供特殊資料格式使用,
常見的有 Date, URL, Email, JSON 等等。

(延伸:如何實作 Custom Scalar Type & 現成自定義 Scalar Type npm 套件:GraphQL Custom Types)

Query 是一種特別的 Object Types

前天提過, GraphQL query 基本上就是選取 Object 的 field 來獲取資料,而 Query Type 本身就是一個 Objcet Type 。
Query Type 在使用上跟一般 Object 沒有不同,不過卻有一項特殊任務: 擔當 Schema 的進入點 (entry point)。

有種門神的概念 XD

所以任何 Type 若是想進入 Schema 被 Client 端 query 到,那麼不管身在第幾層也一定要出現在 Query 的資料結構中。

人外有人、 Query 外還有 Root

當然門神不只 Query 一個,還有 Mutation ( 資料修改、 sql 中的 CREATE & UPDATE ) 與 Subscription (訂閱、 sql 中的 TRIGGER ) 兩位大門神,而能收服這三個門神當作 field 來使用的就是最外層的 Root Types

可以參考 GitHub GraphQL API Explorer

其中右邊 Document Explorer 上面有一個 Root Types 的字樣,意思就是「最上 (外) 層的 Type」。如圖:
Pasted image

另外也可以登入自己的 GitHub 帳號玩一玩, 試著在左側輸入

query {
  viewer {
    login
    name
  }
}

可得到

{
  "data": {
    "viewer": {
      "login": "自己的登入帳號",
      "name": "自己的 GitHub 帳戶名"
    }
  }
}

小問題3: 所以 Root Type 算是一種 Object Type 嗎 ?

那 Resolver 呢 ?

Resolver 部分很簡單,就讓我們接續昨天的例子,順便練習今天所學的部分:

一樣在 index.js

  1. 定義使用者的假資料 (直接看 code)
  2. 在 GraphQL Schema 中定義 User type 並在 Query type 中加入 me field
  3. 在 Resolver 裡的 Query 裡面新增對於 me field 處理的 function,注意名稱一定要對上,目前先回傳第一名使用者
const { ApolloServer, gql } = require('apollo-server');

// 1. 加入假資料
const users = [
  {
    id: 1,
    name: 'Fong',
    age: 23
  },
  {
    id: 2,
    name: 'Kevin',
    age: 40
  },
  {
    id: 3,
    name: 'Mary',
    age: 18
  }
];

// The GraphQL schema
// 2. 新增 User type 、在 Query 中新增 me field
const typeDefs = gql`
  """
  使用者資訊
  """
  type User {
    "識別碼"
    id: ID
    "名字"
    name: String
    "年齡"
    age: Int
  }

  type Query {
    "A simple type for getting started!"
    hello: String
    "取得當下使用者"
    me: User
  }
`;

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
    // 3. 加上 me 的 resolver (一定要在 Query 中喔)
    me: () => users[0]
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

server.listen().then(({ url }) => {
  console.log(`? Server ready at ${url}`);
});

啟動後 (node index.js or npm start) 可測試以下 query :

{
  me {
    id
    name
    age
  }
}

應出現:

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

如圖:

https://imgur.com/yQD5pzS


Schema + Resolver 是不是很簡單啊~
但不是說 Object Type 裡的 field 也可以使用其他 Object Type 嗎?
沒錯但別急!下一篇會教進階一點的 Resolver: Field Resover 的應用以及 Array 等的深入教學,讓你實作「好友功能」


小問題 1:如果 Schema 是菜單、 Resolver 是廚房,那點餐的你是?

點菜的你是前天講的 Query!...是嗎 ? 嚴格來說,點餐的人是 client 端 (手機或筆電),「點菜的請求」 才是 Query 喔!

小問題 2: 所以 business logic 也是 Resolver 的守備範圍囉?

說是守備範圍只算半對,基於 Facebook 官方的建議,較好的做法是 delegate to a dedicated businesslogic layer ,也就是轉交給專門的商業邏輯層。一方面乾濕分離、一方面也維持商業邏輯程式上的唯一正統性 (single source of truth)以利 reuse。可參考1 2 3

小問題3: 所以 Root Type 算是一種 Object Type 嗎 ?

是的。 Root Type 是一個最多擁有三個 field 的 Object Type,其中三個 field 對應到三個 Object Type (Query, Mutation, Subscription)。你甚至也可以說整個 GraphQL Schema 就是一個 Object Type!

備註:在 GraphQL Schema Definition 中,
可以用單行 " 或是多行 """ 來加入文件註釋 (會在文件中呈現),
另外也可以用單行的 # 來表達單純的註釋 (不會在文件中呈現)。
習慣上 type definition 使用 """ 來多行註釋,
field 則是使用 " 來單行註釋。

因為這是練習,所以不一定要打文件註釋,但如果今天是正式開發一個產品,那麼即使專案再趕再急也建議加上文件註釋。避免累積技術債絕對能拯救到未來的你!或是製造更多的工作機會 XD


上一篇
GraphQL 入門: Server Setup X NodeJS X Apollo (寫程式囉!)
下一篇
GraphQL 入門: Schema 與 Resolver 進階功能! (Array, Non-Null, Field Resolver)
系列文
Think in GraphQL30

2 則留言

0
imakou
iT邦新手 5 級 ‧ 2018-10-23 04:38:50

超棒的系列文,不知道為什麼瀏覽人數不多
留言打氣一下!

fx777 iT邦新手 5 級‧ 2018-10-23 21:13:33 檢舉

謝謝!可能我文筆還要再加油哈哈,有些概念可以講的更清楚簡單!
而且除非有興趣要整個學起來~不然單看幾篇其實不好消化,可以理解觀看數較不多的原因 XD

0
PPTaiwan
iT邦新手 5 級 ‧ 2019-02-09 20:01:54

我有一個好奇!! 安裝好 GraphQL 就可以直接與 SQL Server 直接進行查詢的動作了嗎??

fx777 iT邦新手 5 級‧ 2019-02-13 22:40:07 檢舉

恩...這其實是兩個完全不同的概念喔。

GraphQL 只是一層「API」,本質上就是一個有特別功能的 RESTful API ,也就是說 Resolver 之後還是得靠自己去串接 DB 。

不過也有一些工具如 Prisma 標榜直接幫你做好 GraphQL 連接 DB 的部分,讓你專注開發前端。

我要留言

立即登入留言