初識 GraphQL

January 12, 2017

第一次聽到 GraphQL 是在 Facebook 使用 React Native 做了一個 F8 App ,但那時候都把精神都花再學習 Reactjs 上,所以後來也沒有特別去了解。

最近這陣子剛退伍,整理一下了 Instapaper,發現了好幾篇跟 GraphQL 相關的文章(雖然還沒仔細閱讀 😛),GraphQL 真的慢慢變成趨勢了,所以想趁這段空窗期好好學習一下 GraphQL,順便記錄一下學習的過程,希望可以幫助對於 GraphQL 沒有概念的朋友可以對 GraphQL 有多一點認識!

GraphQL 是什麼

GraphQL 是一個 「query language」,是由 Facebook 所提出的,GraphQL 主要是為了解決在 REST 上所遇到的一些問題,待會在下面文章我們會繼續討論到,對於 GraphQL 想要更深入的了解請參考以下連結:

// 假想取得一篇文章的資訊,包含文章作者、時間、日期、留言。
{
  posts {
    id
    author
    title
    date
    comments {
      commentId
      user
      timestamp
    }
  }
}

GraphQL 不是一個程式語言或是框架,它是一個概念和規範,就像 REST 那樣,每個不同的程式語言都有實作自己的 GraphQL,更多請參閱:Code | GraphQL

GraphQL 解決了哪些問題

當我們在設計 API 的時候,通常都會遵循 REST 的風格來設計,在 REST 中,我們把這些不同的資源利用四個動詞:GET、POST、UPDATE、DELETE 來做不同的 state transfer,這些不同的搭配產生的 URI 讓我們來作為 API,例如:

GET /posts  // <- 取得所有文章
POST /posts/ // <- 新增文章
UPDATE /posts/1 // <- 更新 id 為 1 的文章
DELETE /posts/1 // <- 刪除 id 為 1 的文章

上面例子是很簡單的 RESTful API,但是當需求變得比較複雜的時候,可能會用到多個 API,或者是透過 Model 關聯拿回需要的資料,在這個部份有可能會回傳多餘的欄位資料,造成網路資源的浪費;又或者在處理巢狀的 Route 不是這麼的容易,當應用程式越來越複雜時,會發現 Endpoints 會變得非常龐大。

REST vs GraphQL

從上圖來比較 REST 和 GraphQL,當我們透過 REST 來取得 album 的資料和評論時,我們需要透過兩次的 GET 來取得;透過 GraphQL 我們可以明確的告訴 Server 我們需要回傳哪些欄位,一次就可以拿回全部的資料,相較於 REST 更加靈活。

就我對 REST 的了解,和現在目前對 GraphQL 的認識,我的想法是:

RESTful 所有的 resource 都是圍繞著被定義的 route 在進行,而 GraphQL 則是透過定義不同的 Type,然後建立一個 Schema,如 GraphQL 其名,由每個不同的 Type 內的欄位來組織成不同的資料。

以下內容的相關程式碼在:graphql-emoji-for-learning

建立 GraphQL Server

emoji

前面大概解釋了一下 GraphQL 解決了在 REST 上所遇到的一些問題,接下來還是透過實作簡單的範例來感受一下 GraphQL 所帶來的好處,我將使用 JavaScript 來實做並利用一個 emoji.json 來作為假想的 Database,再透過 GraphQL 來取得 emoji 的資料。

首先,我們先完成前置的準備,先建立一個 GraphQL 的 Server:

# 安裝 Koa
$ npm install [email protected] koa-convert koa-mount koa-graphql --save-dev

# or use yarn

$ yarn add -D [email protected]^2.0.0 koa-convert koa-mount koa-graphql

接著,我們安裝由 JavaScript 實作 GraphQL 核心的 package  —  graphql

$ npm install graphql --save

# or use yarn

$ yarn add graphql

完成 package 安裝後,我們來建立一個 schema.jsserver.js 來測試 GraphQL Server:

// schema.js
const { graphql, buildSchema } = require('graphql');

exports.schema = buildSchema(`
  type Query {
    hello: String
  }
`);

exports.rootValue = {
  hello: () => 'Hello world!',
};
// server.js
const Koa = require('koa');
const mount = require('koa-mount');
const convert = require('koa-convert');
const graphqlHTTP = require('koa-graphql');
const { schema, rootValue } = require('./schema');

const app = new Koa();

app.use(mount('/graphql', convert(graphqlHTTP({
  schema,
  rootValue,
  graphiql: true,
}))));

app.listen(3000);

執行 node server.js,接著打開 http://localhost:3000/graphql 確認; GraphiQL 可以讓你很方便執行 query、mutation,並可以在右邊馬上看到你的執行結果,有助於你在 Debug 時找到問題。

如果執行結果如上圖,就代表你的 GraphQL Server 沒問題了!

如何定義 Type

Type System 是 GraphQL 的核心之一,用來定義你的資料類型,在 query 後該回傳什麼類型的資料回來,我們將這些所有 Type 組合起來就是變成了一個 Schema 了!

在 Schema 中,我們都會有一個最基本的 Query Type,它是一個 Key-Value 的形式,每個 Key 會對應到一個 Type;GraphQL 本身內建了五種 Scalar Type,你可以在你的 Schema 內直接使用它們:

  • String
  • Int
  • Float
  • Boolean
  • ID

預設上,每個 GraphQL Type 都可以是 null,可以透過加入 ! 來表示該欄位不可以為 null:

// 以 schema.js 為範例
type Query {
  hello: String!
}

如果需要使用回傳的並不是單一的值,我們可以使用 List 這個,只要把 Type 加入 [] 就可以了:

type Query {
  hello: [String!]! // hello 是 String List,且該欄位和回傳值不能為 null
}

定義 Object Type

這個範例使用了 emoji.json,裡面有一些 Emoji 相關資訊,透過 fetch 將資料抓回來後,做一些處理,並以前面幾筆作為範例 data。

處理完成後的資料結構如下:

{
    1: {
        "no": 1,
        "code": "1F600",
        "char": "😀",
        "name": "GRINNING FACE",
        "date": "2012ˣ",
        "keywords": ["face", "grin"]
    },
    2: {
        "no": 2,
        "code": "1F601",
        "char": "😁",
        "name": "GRINNING FACE WITH SMILING EYES",
        "date": "2010ʲ",
        "keywords": ["eye", "face", "grin", "smile"]
    },
    3: {
        "no": 3,
        "code": "1F602",
        "char": "😂",
        "name": "FACE WITH TEARS OF JOY",
        "date": "2010ʲ",
        "keywords": ["face", "joy", "laugh", "tear"]
    }
}

很多時候我們需要用到的 Type 可能不是單純只有上面的 Scalar Type,所以我們可以定義自己所需的 Type,定義新的 Object Type 就像在定義 type Query 一樣,將你需要的欄位寫上,並給定型別(ex:String、ID ...etc),Type 中的欄位型別也可以是其他你所定義的 Type,並不拘限內建的 Type。

依據整理後的 JSON 結構,新增 type Emoji 並更新我們前面的 schema.js:

const { graphql, buildSchema } = require('graphql');
const data = require('./data.json');

exports.schema = buildSchema(`
  type Emoji {
    no: ID!
    code: String!
    char: String!
    name: String!
    date: String!
    keywords: [String!]!
  }
  type Query {
    emojis: [Emoji!]!
  }
`);

exports.rootValue = {
  emojis: () => Object.keys(data).map(id => data[id]),
};

接著打開 GraphiQL 輸入以下 query,確認執行結果:

query {
  emojis {
    no
    code
    char
    name
    date
    keywords
  }
}
Query 後所產生的 emoji 資料

在前面提到了:「Type 中的欄位型別也可以是其他你所定義的 Type」,聽起來有點繞,讓我們看一下是怎麼回事,假設我們今天有另外一個 data 是長成這樣的:

{
  1: {
    "name": "face",
    "ids": [1, 3],
    "list": [{
        "no": 1,
        "code": "1F600",
        "char": "😀",
        "name": "GRINNING FACE",
        "date": "2012ˣ",
        "keywords": ["face", "grin"]
    }, {
        "no": 3,
        "code": "1F602",
        "char": "😂",
        "name": "FACE WITH TEARS OF JOY",
        "date": "2010ʲ",
        "keywords": ["face", "joy", "laugh", "tear"]
    }]
  },
  2: {
    "name": "grin",
    "ids": [1, 2],
    "list": [{
        "no": 1,
        "code": "1F600",
        "char": "😀",
        "name": "GRINNING FACE",
        "date": "2012ˣ",
        "keywords": ["face", "grin"]
    }, {
        "no": 2,
        "code": "1F601",
        "char": "😁",
        "name": "GRINNING FACE WITH SMILING EYES",
        "date": "2010ʲ",
        "keywords": ["eye", "face", "grin", "smile"]
    }]
  }
}

有注意到 list 這個 key 嗎?是否和我們所定義的 type Emoji 相似,沒錯!所以我們可以這樣去定義這個資料的 type:

type XXX {
  name: String!
  ids: [Int!]!
  list: [Emoji!]!
}

傳入參數

在 GraphQL 也可以使用傳入參數的搜尋方式,我們利用上面的範例再做一些小改進,首先我們在 type Query 的部份再新增一個 relatedEmoji(keyword: String!)

type Query {
  emojis: [Emoji!]!
  relatedEmoji(keyword: String!): [Emoji!]!
}

我們將 keyword 傳給 Resolver Function,讓 Resolver Function 去作應對的處理:

exports.rootValue = {
  emojis: () => { ... },
  relatedEmoji: ({ keyword }) => Object.keys(data)
    .map(id => data[id])
    .filter(emoji => emoji['keywords'].includes(keyword) ? emoji : null),
};

What’s resolver function? resolver function 就是當我們這個 field(ex:relatedEmoji) 在執行時,所對應到的 f unction 來處理並產生結果。

我們再透過下面的方式在 query 一次,確認 query 的結果:

透過傳送參數到 keyword 來進行 query,我們可以找到所 keywords 內有 grin 的資料

Mutation

Mutation 翻譯過來的意思是:突變、變化;當我們在新增、修改、刪除資料時,我們在這都稱這個動作為為 Mutate,而在 GraphQL 主要的動作就是 QueryMutation,在這個部份簡單介紹如何使用 Mutation。

首先,我們必須在 buildSchema 新增另一個 Top-Level Type  —  Mutation

exports.schema = buildSchema(`
  type Emoji { ... }
  type Query { ... }
  type Mutation { ... }
`)

新增 Emoji

這裡我們先來實作新增(Create)資料的部份,在 SQL 中也就是所謂的 Insert,我們更新 type Mutation 的內容:

type Mutation {
  createNewEmoji(input: EmojiInput): Emoji
}

createEmoji(input: EmojiInput) 的地方這裡有個小細節,這裡的 EmojiInput 是定義一個 input 的 type( input-types),我們可以預期得到一個我們所要的輸入 type:

input Emoji {
  code: String!
  char: String!
  name: String!
  date: String!
  keywords: [String!]!
}

所以在新增一筆新的 Emoji 之後,回傳的 Type 是 Emoji,在這裡可以很明顯感受到 GraphQL 的 Type System 所帶來的好處;接著我們來實作 createNewEmoji 的 Resolver Function:

exports.rootValue = {
  emojis: () => { ... },
  relatedEmoji: ({ keyword }) => { ... },
  createNewEmoji: ({ input }) => {
    const newEmoji = data[Object.keys(data).length + 1] = input;
    const id = Object.keys(data).length;
    newEmoji['no'] = id;
    return newEmoji;
  },
};

打開 GraphiQL 輸入以下來新增一筆資料,並確認執行結果:

mutation {
  createNewEmoji(input: {
    code: "1F61C",
    char: "😜",
    name: "FACE WITH STUCK-OUT TONGUE AND WINKING EYE\n≊ face with stuck-out tongue & winking eye",
    date: "2010ʲ",
    keywords: ["eye","face","joke","tongue", "wink"]
  }) {
    no
    code
    char
    name
    keywords
  }
}
新增一筆 Emoji 資料

看起來是新增成功了,我們透過 query 確認是否正確:

成功新增第四筆 Emoji 的資料!

更新 Emoji

寫到這發現篇幅有點長了😮,這邊就繼續延續上面的部份,直接實作更新(Update)資料的部份。

我們要在 type Mutation 再新增一個 updateEmoji(id, input),給定我們要更新資料的 id 和所要更新的資料:

type Mutation {
  createNewEmoji(input: EmojiInput): Emoji
  updateEmoji(id: ID!, input: EmojiInput): Emoji
}

exports.rootValue 新增 updateEmoji 的 Resolver Function:

exports.rootValue = {
  emojis: () => { ... },
  relatedEmoji: { ... },
  createNewEmoji: { ... },
  updateEmoji: ({ id, input }) => {
    data[id] = input;
    data[id]['no'] = id;
    return data[id];
  },
};
修改剛剛新增的第四筆 Emoji 資料。

再使用 query 來確認第四筆的 Emoji 資訊是否被更新了:

修改剛剛新增的第四筆 Emoji 資料。

刪除 Emoji

最後就是刪除(Delete)資料了,我們在 type Mutation 新增一個 deleteEmoji(id) ,結果回傳的是刪除後其他的資料:

type Mutation {
  createNewEmoji(input: EmojiInput): Emoji
  updateEmoji(id: ID!, input: EmojiInput): Emoji
  deleteEmoji(id: ID!): [Emoji]
}

更新 exports.rootValue,新增 deleteEmoji 的 Resolver Function:

exports.rootValue = {
  emojis: () => { ... },
  relatedEmoji: ({ keyword }) => { ... },
  createNewEmoji: ({ input }) => { ... },
  updateEmoji: ({ id, input }) => { ... },
  deleteEmoji: ({ id }) => {
    delete data[id];
    return Object.keys(data).map(id => data[id]);
  }
};
刪除 id 為 4 的 Emoji 資料前。

執行刪除資料後的回傳結果如下:

可以發現第四筆資料已經被我們刪除了!

介紹 GraphQL 大概就到這裡了,把我們最常使用的 CRUD 給走過了一遍,當然 GraphQL 的功能還不只這樣,還有一些像是:Fragments、Interfaces…etc,當然更多相關的資料還是到 GraphQL 官方網站去學習囉!

這次對 GraphQL 的學習算是相當的淺的,還有許多沒有 touch 到的地方希望有時間再來學習,這篇文章是一個 GraphQL 初學者所寫的,我想內容應該會有不少錯誤(笑),如果有任何表達或是思考上的錯誤,歡迎糾正我,感謝你(妳)的閱讀!