iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 6
2
Modern Web

Think in GraphQL系列 第 6

GraphQL 入門: Schema 與 Resolver 進階功能! (Array, Non-Null, Field Resolver)

header

今天要來介紹一些進階的 Type syntax 並且實作 Resolver 一個強大的特性,也就是 Field Resover 。這一個強大的設計讓 GraphQL 在資料處理上做到了相當好的隔離效果,也讓 GraphQL 真正能做到 Data Driven (依照資料需求) 而非 Database Driven。


好用的 Object Type Syntax

經過昨天的例子,想必大家已經抓到 Object Type 的精髓,接下來我們要錦上添花,介紹更多小工具 (syntax) 來增強 Object Type 的功能!我將會介紹:

  1. Array Type
  2. Non-Null

Array Type Syntax []: 一個不夠,可以放兩個啊

在開發 User Type 的過程中,如果遇到像 friends 這種 field ,本質上是另一個 User Type 但需要 用 array 形式展現,只要在定義時用 [] 來圍繞 type 就可以了。
friends: [User] 就代表一個 User Type 的 Array 。
同樣地, names: [String] 代表 names 這個 field 值會是一個 String Array。

接下來讓我們看例子:

type User {
 ...
 "朋友列表 ([] 代表 array 之意)"
 friends: [User]
}

被 Query 到時會得到

"data": {
  "me": {
    "friends": [
      {
        ...
      },
      {
        ...
      }
    ]
  }
}

可發現 friends: [User] 回傳的值就會是一個 Object Array

Not-null Syntax ! 保證期待不落空

在定義時後面加上 ! 代表說此項 field 的值不能為 null,比如定義 User 時,正所謂「 DB 在走、 ID 要有」,因此 id 是不可缺少。

type User {
 id: ID!
 ...
}

如此一來,若是 client 端 query 到 id 這個 field 時, server 端就不能讓 id 為 Null,否則 GraphQL 就會報錯。 如圖:
Pasted image

Non-null 好處是讓前端可以確保一定可以從特定 field 得到 non-null 的資料,減少開發的不確定性。

小問題 1: 所以每次 query 到 User Type 時都一定要選 id 這項 field 嗎?
解答在文章末

Array Type 配 Non-null 蹦出新滋味

這時候難題來了,如果 Array Type 配上 Not-null 又會變成怎樣呢? 讓我們看以下的例子

  1. field: User

    [O] null
    

    field 為 nullable

  2. fields: [User]

    [O] null, [], [null]
    

    fields 為 nullable, array 裡的值也為 nullable

  3. fields: [User!]

    [O] null, []
    [X] [null], [obj, null, obj]
    

    fields 為 nullable, array 裡的值為 non-null

  4. fields: [User!]!

    [O] [], [{ id: ... }]
    [X] null, [null], [obj, null, obj]
    

    fields 為 non-null, array 裡面的值也為 non-null

還有更基八一點的: [[User!]]! XDD, 這個就留給讀者自行思考囉~~

但 Non-Null 就完美無缺嗎? 別忘了 GraphQL 要服務各式各樣的 Client 端,不是每個 Client 端的 Query 需求都相同,而當 Client 端相信你給的值的時候卻獲得一個 Null ...他也會無情的回報你一堆 bug 。因此一旦修改 Not-Null field 就會是 Breaking Change。

實戰經驗談:添加 Not-Null 時都要非常小心,因為一旦修改就會是 breaking change。
所以建議是:剛開始設計時,除了 ID 以外的欄位都不要加上 !

延伸閱讀:When To Use GraphQL Non-Null Fields

配合以上的 Array Type Syntax ,我們可以擴展我們的 Schema:

type User {
  id: ID!
  name: String
  age: Int
  friends: [User]
}

type Query {
  hello: String
  me: User
  users: User
}

接下來就教大家如何實作以上的 Resolver。

Field Resolver: 做到資料處理概念分離

上面 Query 中 users 的實作很簡單,只要在 Resolver 新增一個對應的 function 回傳 user data 即可 ;
難就難在 User type 中的 friends 到底要怎麼處理?

這裡有兩種方法,第一種是回傳的 user data 需附上 friends 的所有資料如下:

const resolvers = {
  ...
  me: () => ({
    id: 1,
    name: 'Fong',
    age: 23,
    friends: [
      { id: 2, name: 'Kevin', age: 40, friends: [...] },
      { id: 3, name: 'Mary', age: 18, friends: [...] },
    ]
  })
}

此時有沒有發現怪怪的地方?沒錯這裡有兩大問題,第一個是如果今天需要 friendsfriendsfriends 話,那你的資料複雜度會高到難以處理 ; 第二是如果你是使用 Relational Database ,你就要先實作一系列的 join 來確保資料的完整性,但最後可能根本沒有被 query 到 friends 這個 field , 另外也無法處理層數更深的 query 。

於是就需要我們的第二種處理方式 : Field Resolver 。
Field Resolver 顧名思義就是針對單一 Field 做資料取得的實作,而其實 Resolver Function 中的 hello, me, user 都算是 Field Resolver ,不過我們將使用這個概念在更細的資料上,也就是

每一個 Field 都可以擁有自己的 Field Resolver ,不管是 Object Type 或是 Scalar Type

小問題 2 : 所以如果 GraphQL 在處理回傳資料時發現有一個 field 早已有值卻也有自己的 Field Resolver 的話是使用誰的結果?

Coding Part

以下就進入 Coding 部分直接帶大家了解:

  1. 在假資料中補充朋友資訊
  2. 在 Schema 添加新 fields
  3. 在 Resolver 中需
    (1) 在 Query 裡新增 users
    (2) 新增 User 並包含 friends 的 field resolver
const { ApolloServer, gql } = require('apollo-server');

// 1. 在假資料中補充朋友資訊
const users = [
  { id: 1, name: 'Fong', age: 23, friendIds: [2, 3] },
  { id: 2, name: 'Kevin', age: 40, friendIds: [1] },
  { id: 3, name: 'Mary', age: 18, friendIds: [1] }
];

// The GraphQL schema
// 2. 在 Schema 添加新 fields
const typeDefs = gql`
  """
  使用者
  """
  type User {
    "識別碼"
    id: ID
    "名字"
    name: String
    "年齡"
    age: Int
    "朋友們"
    friends: [User]
  }

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

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    hello: () => 'world',
    me: () => users[0],
    // 3-1 在 `Query` 裡新增 `users`
    users: () => users
  },
  // 3-2 新增 `User` 並包含 `friends` 的 field resolver
  User: {
    // 每個 Field Resolver 都會預設傳入三個參數,
    // 分別為上一層的資料 (即 user)、參數 (下一節會提到) 以及 context (全域變數)
    friends: (parent, args, context) => {
      // 從 user 資料裡提出 friendIds
      const { friendIds } = parent;
      // Filter 出所有 id 出現在 friendIds 的 user
      return users.filter(user => friendIds.includes(user.id));
    }
  }
};

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

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

這邊要補充一下大家一定會有的疑問: friends(parent, args, context) 是什麼? 這是 GraphQL 會自動幫我們傳入的參數,其實在 Query 中的 hello, me, users 三個 function 的參數裡也會是這個形式,不過為了閱讀方便且並未用到所以我就省略了。

那我們就來啟動 Server 然後再輸入 Query 吧:

{
  me {
    name
    friends {
      id
      name
    }
  }
}

會得到

{
  "data": {
    "me": {
      "name": "Fong",
      "friends": [
        {
          "id": "2",
          "name": "Kevin"
        },
        {
          "id": "3",
          "name": "Mary"
        }
      ]
    }
  }
}

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

此外也可以 query users 看會得到什麼結果喔 !

有人一定會說,每遇到一次 field resolver 就要訪問一次 database 的話不是會造成效能負擔?沒錯,但基本上一定比你 RESTful 的 multiple round trip 來得省時。
但如果你一次要拿超大量資料的話,仍會造成 N + 1 problem 的問題,不過別擔心,之後會教大家如何克服 ,可先參考 這篇 (基本上公司系統有使用過,效果不賴)

練習題

請 query 出所有 user 的 id, name 以及 friendsage 。答案 點開圖


今天就到這邊,想必大家都已經掌握了基本的 GraphQL 技巧,可以再學習更進階的 query 技巧以及相對應的 Schema + Resolvers ,那就明天見~


小問題 1: 所以每次 query 到 User Type 時都一定要選 id 這項 field 嗎?

Not-Null ! 只代表說 如果有選到,那 Server 對於這項 field 就不能給出規定的 Type 以外的選項包括 Null,所以 query 時若沒有挑選這項 field ,就不會有影響。

小問題 2 : 所以如果 GraphQL 在處理回傳資料時發現有一個 field 早已有值卻也有自己的 Field Resolver 的話是使用誰的結果?

在處理程序上,GraphQL 是一層層處理,在以上例子中 GraphQL 會先處理 Queryme field Resolver ,拿到資料後才會進入 User 的 field Resolver。所以決定最終值的當然是後者。

小問題 3: 咦?所以說好的對於 Scalar Type 的 Field Resolver 呢

通常很少會對 Scalar Type 做 Field Resolver ,等下一篇介紹 Argument 後才比較有機會。但還是可以給你範例:

const resolver = {
  ...,
  User: {
    ...,
    name: (parent, args, context) => {
      const date = new Date();
      // 假如今天萬聖節
      if (date.getMonth() + 1 === 10 && date.getDate() === 31) {
        return parent.name + ' ~~ Happy Halloween';
      }
      return parent.name;
    }
  }
}

上一篇
GraphQL 入門:初次實作 Schema 與 Resolver
下一篇
GraphQL 入門: Arguments, Aliases, Fragment 讓 Query 更好用 (進階 Query)
系列文
Think in GraphQL30

尚未有邦友留言

立即登入留言