iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 8
2

header

今天要介紹 GraphQL 三大支柱之二的 Mutation 。相比 Query 負責資料的取得,凡是資料更改或新增都屬於 Mutation 的負責範圍
與 Query 相同的是, Mutation 一樣是整個 GraphQL Schema 的 entry point ,並且也需要先在後端 Schema 定義好才能供前端 query ,所以我會先介紹如何在 Schema 中定義 Mutation ,然後才講解如何使用 Mutation Query 。


開始前...先建立個 User + Post 架構

開始前我們先在原先的 Schema 中加入一個新的 Post Type 並在 User Type 中新增 posts fields ,同時也在 resolver 加入相關 fucntion
(為 demo 方便,我先移除一些較不重要的欄位)

  • Mock Data

一筆貼文內容有 id, 作者 id (authorId), 標題 (title), 內文 (content) 以及按讚者 id (likeGiverIds)

const posts = [
  { id: 1, authorId: 1, title: "Hello World!", content: "This is my first post.", likeGiverIds: [2] },
  { id: 2, authorId: 2, title: "Good Night", content: "Have a Nice Dream =)", likeGiverIds: [2, 3] },
  { id: 3, authorId: 1, title: "I Love U", content: "Here's my second post!", likeGiverIds: [] },
];
  • Schema Part
const typeDefs = gql`
  type User { ... }

  """
  貼文
  """
  type Post {
    "識別碼"
    id: ID!
    "作者"
    author: User
    "標題"
    title: String
    "內容"
    content: String
    "按讚者"
    likeGivers: [User]
  }

  type Query { ... }
`;

以及加上 Resolver:

// Helper Functions
const findUserById = id => users.find(user => user.id === id);
const findUserByName = name => users.find(user => user.name === name);
const filterPostsByAuthorId = authorId =>
  posts.filter(post => post.authorId === authorId);

// 1. 新增 User.posts field Resovler
// 2. 新增 Post Type Resolver 及底下的 field Resolver
const resolvers = {
  Query: { ... },
  User: {
    ...,
    // 1. User.parent field resolver, 回傳屬於該 user 的 posts
    posts: (parent, args, context) => {
      // parent.id 為 userId
      return filterPostsByAuthorId(parent.id);
    }
  },
  // 2. Post type resolver
  Post: {
    // 2-1. parent 為 post 的資料,透過 post.likeGiverIds 連接到 users
    likeGivers: (parent, args, context) => {
      return parent.likeGiverIds.map(id => findUserById(id));
    },
    // 2-2. parent 為 post 的資料,透過 post.author
    author: (parent, args, context) => {
      return findUserById(parent.authorId);
    }
  }
};

有了一個文章系統後,讓我們走下去:

Mutation - Server Part

首先來看我們需要哪些 Mutation

  1. addPost(title, content) : 新增文章。參數為 title (標題)、 content (內文)
  2. likePost(postId) : 喜歡文章。參數為該 post 的 id

接下來需要做兩件事,將以上 mutation 加入 Schema 的 Mutation 定義以及新增相關的 Resolver function。

Mutation Server Part - Schema

進入程式:
(上面提過的程式會先忽略)

const typeDefs = gql`
  type User { ... }
  type Post { ... }
  type Query { ... }

  # Mutation 定義
  type Mutation {
    "新增貼文"
    addPost(title: String!, content): Post
    "貼文按讚 (收回讚)"
    likePost(postId: ID!): Post
  }
`;

Mutation Server Part - Resolver

接下來是 Resolver function。
因為 mutation 都預設是目前使用者在做操作 (畢竟使用者 A 可以幫 B 新增文章也很怪) ,所以這裡會先在上面定義 meId 來代表目前使用者的 id ,之後介紹到 login 時會有更好的應用方式。

const meId = 1;
const findPostById = id => posts.find(post => post.id === id);

const resolvers = {
  Query: { ... },
  // Mutation Type Resolver
  Mutation : {
    addPost: (root, args, context) => {
      const { title, content } = args;
      // 新增 post
      posts.push({
        id: posts.length + 1,
        authorId: meId,
        title,
        content,
        likeGivers: []
      });
      // 回傳新增的那篇 post
      return posts[posts.length - 1];
    },
    likePost: (root, args, context) => {
      const { postId } = args;
      const post = findPostById(postId);
      if (!post) throw new Error(`Post ${psotId} Not Exists`);

      if (post.likeGiverIds.includes(meId)) {
        // 如果已經按過讚就收回
        const index = post.likeGiverIds.findIndex(v => v === userId);
        post.likeGiverIds.splice(index, 1);
      } else {
        // 否則就加入 likeGiverIds 名單
        post.likeGiverIds.push(meId);
      }
      return post;
    },
  },
  User: { ... },
  Post: { ... }
};

可以看到, Mutation 的定義方式與 Query 一模一樣且 field 名稱也要對上。

而 Mutation 有趣點在於執行操作後它還可以回傳資料,讓 client 端做完 muation 後可以直接得到更新後的資料且 field 可自行挑選。

此外如果 Mutation field 宣告參數時, Scalar Type 的參數可以直接宣告,但如果要使用 Object 格式的參數,與 query 不同,需要額外再宣告 Input Object Type 才能使用 (下面會再介紹) 。

Mutation - Client Part

接下來是 Query Part ,格式上可參考下圖。

Imgur

接著就讓我們來寫 mutation query

# Operation Type 為 mutation 時不可省略
mutation {
  addPost(title: "Mutation Is Awesome", content: "Adding Post is like a piece of cake") {
    id
    title
    author {
      name
    }
  }
}

得到的 Response

{
  "data": {
    "addPost": {
      "id": "4",
      "title": "Mutation Is Awesome",
      "author": { "name": "Fong" }
    }
  }
}

可見圖:
Imgur

  1. 參數 type 必須正確,在這裡 title 為 Non-Null String (必填)、 content 為 Nullable String (選填)。若格式不符合會直接吐 Error ,連 Resolver 都進不去。
  2. 可以如 Query 一樣加上 alises 就可以一次執行很多筆 mutation ,不過有鑒於 Mutation 這類更改資料的行為是可能互相影響的,因此在執行上

Query Fields 是平行執行的,然而 Mutation 則是一筆一筆照順序執行

如果你已經成功發出 mutation ,可以試試 query 自己的 post 看貼文數量是否有增加 ! 如圖:
img

如果你懶得打扣可以看看我的 GraphQL Playground Example

經驗談: 說實在, Mutation 與 Query 兩者最主要的差別在語意上,如果今天你使用 query 然後後台把資料大改一通也沒有人會阻止你。所以使用 GraphQL 時務必要遵守好規範,對於資料的修改都要非常謹慎,不然自己挖的坑自己同事要負責填。

Input Object Type 介紹

當參數列所需的資料越來越複雜、到時候長度就會讓你看不完...
這時 Input Type 提供了 Object Type 形式的 Argument ,讓我們來看如何使用。可參考下圖。
Imgur

接下來看範例~~

Input Object Type 範例

const typeDefs = gql`
...
input AddPostInput {
  title: String!
  content: String
}

Mutation {
  addPost(input: AddPostInput!): Post
}
`;

這邊非常重要, Input Object Type 與 Object Type 完全不同,一個是傳入 Argument 作為 Input ,一個是用於資料索取展示

然後是 Resolver 部分:

const resolvers = {
  ...,
  Mutation: {
    ...,
    // 需注意!args 打開後第一層為 input ,再進去一層才是 title, content
    addPost: (root, args, context) => {
      const { input } = args;
      const { title, content } = input;
      const newPost = {
        id: posts.length + 1,
        authorId: meId,
        title,
        content,
        likeGivers: []
      };
      posts.push(newPost);
      return newPost;
    },
  },
}

接著在 GraphQL Playground 輸入 mutation:

# Operation Type 為 mutation 時不可省略
mutation AddPostAgain ($input: AddPostInput) {
  addPost(input: $input) {
    id
    title
    author {
      name
    }
  }
}

---
Variables
{
  "input": {
    "title": "Input Object Is Awesome",
    "content": "ZZZZZZZZZZZZZ"
  }
}

就會得到跟以上一樣的回覆

{
  "data": {
    "addPost": {
      "id": "5",
      "title": "Input Object Is Awesome",
      "author": { "name": "Fong" }
    }
  }
}

Input Object Type 就是這麼簡單~~

經驗談: 公司剛開發時對於 GraphQL 的掌握度還不高,所以程式中 input object type 與 object type 的命名相似且組成的 field 也幾近相同 (ex: 同時會有 NewOrder, UpdateOrder, Order ),使得我一開始學習上常常搞混兩者,後來加入一些規範及重構後才漸漸將兩者的概念分清楚,這邊想講一些推薦的 convention。

命名部分,推薦每一支 mutation 都新增一支專屬的 input object type ,並可考慮在命名上採用 [mutation name] + Input 的形式 (如addPostInput )方便辨認。

另外一些 field 如 id, createdAt, createdBy, updatedBy 等等可透過 server 自動給值的欄位就可以不用出現在 input object type 中,統一讓 server 去計算,減少需要考慮的 field 的數量。


有了前面的基礎,相信學習 Mutation 也是輕鬆就上手!不過這幾天一下子吸收了這麼大量的知識,可能已經開始吃不消了,所以明天我們就來依靠現有的技術來打造一個實用的 GraphQL API 吧!

那要做什麼應用呢?那就做一個部落格社交系統好了!


Reference:


上一篇
GraphQL 入門: Arguments, Aliases, Fragment 讓 Query 更好用 (進階 Query)
下一篇
GraphQL 入門: 深度解析 Field Resolver 的參數: (parent, args, context)
系列文
Think in GraphQL30

尚未有邦友留言

立即登入留言