---
# System prepended metadata

title: GraphQL

---

# GraphQL

[GraphQL 官網](https://graphql.org/)
[Apollo Server v3](https://www.apollographql.com/docs/apollo-server/)
[Github Code](https://github.com/WeiYun0912/GrpahQL-Basic)
:::success

:bookmark: 書籤

[TOC]
:::
## 初始設定

在開發的時候建議使用nodemon，這樣我們變更檔案存檔後就不用再重新啟動server。

要使用GrpahQL必須先安裝以下套件

```javascript=
npm install apollo-server
```

Apollo Server裡面就包含使用GraphQL的工具與Server。

安裝完以後直接將Apollo Server啟動

***server.js***

```javascript=
const { ApolloServer } = require("apollo-server");

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

server.listen({ port: process.env.PORT || 4000 }).then(({ url }) => {
  console.log(`Server is ${url}`);
});
```

這邊如果直接執行該程式會報錯，因為我們還沒用定義type和resolvers。

## 範例資料
因為GraphQL是查詢語言並不是資料庫，所以這邊我們必須要提供資料來做查詢。

本範例省略掉資料庫的連線設定與使用，如果需要連線資料庫的話推薦使用Mongo DB。

***tempData.js***
```json=
const Books = [
  {
    id: 1,
    name: "WeiWei Adventure Ep1",
    author: "Wei",
    publish: "Wei's Publish",
  },

  {
    id: 2,
    name: "WeiWei Adventure Ep2",
    author: "Wei",
    publish: "Wei's Publish",
  },

  {
    id: 3,
    name: "WeiWei Adventure Ep3",
    author: "Wei",
    publish: "Wei's Publish",
  },
  {
    id: 4,
    name: "YunYun Adventure Ep1",
    author: "Yun",
    publish: "Yun's Publish",
  },
  {
    id: 5,
    name: "YunYun Adventure Ep2",
    author: "Yun",
    publish: "Yun's Publish",
  },
  {
    id: 6,
    name: "YunYun Adventure Ep3",
    author: "Yun",
    publish: "Yun's Publish",
  },
];

module.exports = Books;
```

## 型態
要定義type之前當然要知道GraphQL有提供哪些type給我們使用

==Int== : 32-bit的整數型態

==Float== : 符號雙精度的小數點型態

==String== : UTF-8 字串型態

==Boolean== : 布林值型態 (true or false)

==ID== : 標識符

## 定義type與resolvers

知道了有哪些型態以後就來先定義我們的type吧，這邊先以書本為例子，
一本書會有書名、作者、出版社，當然，每本書都是獨一的，所以也必須加上ID。

**type Query**裡面要放的是我們想查詢的資料與回傳格式，可以看到程式第12行，我們希望輸入books查詢後，回傳的是陣列型態，而該陣列裡面的包含的型態就是程式碼第4行~第9行定義的型態。


***typeDefs.js***
```javascript=
const { gql } = require("apollo-server");

const typeDefs = gql`
  type Book {
    id: ID!
    name: String!
    author: String!
    publish: String!
  }

  type Query {
    books: [Book!]
  }
`;

module.exports = typeDefs;
```

定義好type以後還沒結束，我們必須來實作resolvers，這樣當我們下達指定的條件server才會回傳值給我們，本範例是使用上面的範例資料來做查詢。

***resolvers.js***
```javascript=
const Books = require("../tempData");

const resolvers = {
  Query: {
    books: () => {
      return Books;
    },
  },
};

module.exports = resolvers;
```

這邊我們先簡單的把範例資料回傳就好，這邊就如同前面提到的，回傳型態會是Array。


## 啟動Server
定義好type和resolvers以後將他引入至server.js，之後就可以啟動server了！

server預設的port是4000

***server.js***

```javascript=
const { ApolloServer } = require("apollo-server");
const resolvers = require("./schema/resolvers");
const typeDefs = require("./schema/typeDefs");

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

server.listen().then(({ url }) => {
  console.log(`Server Url : ${url}`);
});
```

```javascript=
nodemon server.js
```

![](https://i.imgur.com/4OLUyts.png)

接著在瀏覽器就可以輸入localhost:4000進入到後台，畫面如下：
![](https://i.imgur.com/2tfzPh3.png)

可以在左手邊的Fields看到我們剛剛定義的books！

現在我們直接來下query，來看回傳的是不是範例資料內的資料~

我們直接在**Operations**輸入
```graphql=
query{
  books{
    id
    name
    author
    publish
  }
}
```
也就是我們一開始定義的型態，按下Run以後回傳的值如下：

![](https://i.imgur.com/W1ocQZE.png)

而我們也可以定義query的名稱，定義的方式非常簡單，只要在query後面加上要自訂的名稱就好，如以下：
```graphql=
query Books{
  books{
    id
    name
    author
    publish
  }
}
```

![](https://i.imgur.com/XiMS0MD.png)


## Parent

GrpahQL也可以有階層式的查詢，在以上的範例資料不好做示範，所以這邊我們來更新一下我們的範例資料，這邊因為只是簡單的示範，所以欄位名稱上先不要求嚴謹，在以下範例資料我們把作者獨立出來，並把書本的作者欄位改成對應的ID值。

***tempData.js***
```json=
const Authors = [
  {
    id: 1,
    name: "Wei",
  },
  {
    id: 2,
    name: "Yun",
  },
];

const Books = [
  {
    id: 1,
    name: "WeiWei Adventure Ep1",
    author: 1,
    publish: "Wei's Publish",
  },

  {
    id: 2,
    name: "WeiWei Adventure Ep2",
    author: 1,
    publish: "Wei's Publish",
  },

  {
    id: 3,
    name: "WeiWei Adventure Ep3",
    author: 1,
    publish: "Wei's Publish",
  },
  {
    id: 4,
    name: "YunYun Adventure Ep1",
    author: 2,
    publish: "Yun's Publish",
  },
  {
    id: 5,
    name: "YunYun Adventure Ep2",
    author: 2,
    publish: "Yun's Publish",
  },
  {
    id: 6,
    name: "YunYun Adventure Ep3",
    author: 2,
    publish: "Yun's Publish",
  },
];

module.exports = { Authors, Books };
```

在定義type的地方也要改一下

***typeDefs.js***
```javascript=
const { gql } = require("apollo-server");

/* Book內的author型態 改為 [Author!] */
const typeDefs = gql`
  type Book {
    id: ID!
    name: String!
    author: [Author!]
    publish: String!
  }

  type Author {
    id: ID!
    name: String!
  }

  type Query {
    books: [Book!]
    book(id: ID): Book
  }
`;

module.exports = typeDefs;
```


我們來看一下階層式查詢的範例

```graphql=
query Books {
  books {
    id
    name
    publish
    author {
      id
      name
    }
  }
}
```

型態定義好了，那現在我們要怎麼查詢到書本的作者呢?

這時候就輪到 **Parent** 登場了~ 直接來看範例

我們必須在Book型態底下去實作author的resolver。

***resolvers.js***

```javascript=
const { Authors, Books } = require("../tempData");

const resolvers = {
  Query: {
    books: () => {
      return Books;
    },

    book: (parent, args) => {
      return Books.find((book) => book.id === +args.id);
    },
  },

  Book: {
    author: (parent) => {
      console.log(parent);
    },
  },
};

module.exports = resolvers;
```

這邊我們先不return值，先在後台的**Operation**輸入以下Query，來查看parent裡面的值是什麼。

```graphql=
query Books {
  books {
    id
    name
    publish
    author {
      id
      name
    }
  }
}
```

執行後，我們會在TERMINAL看到以下畫面

![](https://i.imgur.com/5bziUoM.png)

這時候"Parent"這個單字應該很明顯了，就是取得父階層的值，那現在我們把resolvers實作完吧！

***resolver.js***
```javascript=
const { Authors, Books } = require("../tempData");

const resolvers = {
  Query: {
    books: () => {
      return Books;
    },

    book: (parent, args) => {
      return Books.find((book) => book.id === +args.id);
    },
  },

  Book: {
    author: (parent) => {
      return Authors.find((author) => author.id === parent.author);
    },
  },
};

module.exports = resolvers;
```

![](https://i.imgur.com/lDVEbuu.png)


## Args
假設今天我們想要單獨查詢書本的資訊要怎麼做呢?
一般的作法都是利用 **id** 來找尋資料，而在GraphQL我們一樣可以靠外部參數來找尋特定的資料，來看以下範例：

在type定義時必須多加一條規則，也就是程式碼第13行，括號的意思是接收的參數名稱與參數型態。

***typeDefs.js***
```javascript=
const { gql } = require("apollo-server");

const typeDefs = gql`
  type Book {
    id: ID!
    name: String!
    author: String!
    publish: String!
  }

  type Query {
    books: [Book!]
    book(id: ID): Book
  }
`;

module.exports = typeDefs;
```

同樣的我們也必須在resolvers實作，先來看如何傳入外部參數。

在下方的Variables內定義bookId，名稱可以自己取，但要跟query傳入的名稱一樣。

![](https://i.imgur.com/GiVNlXo.png)

***resolvers.js***
```javascript=
const Books = require("../tempData");

const resolvers = {
  Query: {
    books: () => {
      return Books;
    },

    book: (parent, args) => {
      console.log(args); // output : { id : '1' }
      return Books.find((book) => book.id === +args.id);
    },
  },
};

module.exports = resolvers;
```

## Context

Context有點像是Middleware，Context可以拿來做一些驗證，像是JWT，直接來看範例。

要啟用Context必須在Server做設定，這邊我們直接回傳request。

***server.js***
```javascript=
const { ApolloServer } = require("apollo-server");
const resolvers = require("./schema/resolvers");
const typeDefs = require("./schema/typeDefs");

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    return req;
  },
});

server.listen().then(({ url }) => {
  console.log(`Server Url : ${url}`);
});
```

回傳的context可以在resolvers做使用

***resolvers.js***

```javascript=
const { Authors, Books } = require("../tempData");

const resolvers = {
  Query: {
    books: (parent, args, context) => {
      console.log(context.headers);
      return Books;
    },
  },
};

module.exports = resolvers;
```

直接在後台查詢books後查看我們的TERMINAL。

![](https://i.imgur.com/q2BM0We.png)


## Context 2

當然，我們也可以直接將範例資料傳入Context，之後就不用在resolvers去引入我們的範例資料！

***server.js***
```javascript=
const { ApolloServer } = require("apollo-server");
const resolvers = require("./schema/resolvers");
const typeDefs = require("./schema/typeDefs");
const { Books } = require("./tempData");

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

server.listen().then(({ url }) => {
  console.log(`Server Url : ${url}`);
});
```

***resolvers.js***
```javascript=
const resolvers = {
  Query: {
    books: (parent, args, context) => {
      return context.Books;
    },
  },
};

module.exports = resolvers;

```
![](https://i.imgur.com/Ch6sQDM.png)


## Fragment
當回傳值太多的時候，或是多個type回傳的值都一樣時可以使用**Fragment**來簡化，直接來看以下範例。

假設要回傳的值有以下：

```graphql=
query Books{
    books{
        id
        name
        author
        publish
    }
}
```

我們可以利用**Fragment**來將要回傳的值個別獨立出來。

```graphql=
query Books{
    books{
        ...BookFragement
    }
}

fragment BookFragement on Book{
    id
    name
    author
    publish
}
```

![](https://i.imgur.com/ujs5Idd.png)


## Interface

善用**Interface**能夠有效管理型態，Interface type可以提供一組fields讓不同的物件(Object)共享，我們直接來看範例。


我們用以下範例來看，如果要在GraphQL回傳下面的範例資料，則必須在定義type的時候把所有需要的欄位補上。

***tempData.js***
```json=
const Animals = [
  {
    name: "狼蛛",
    footLength: 123,
  },
  {
    name: "奇異鳥",
    footLength: 123,
    wingLength: 456,
    wing: false,
  },
];
```

像是這樣

***typeDefs.js***
```javascript=
const { gql } = require("apollo-server");

const typeDefs = gql`
  type Animal {
    name: String
    footLength: Int
    wingLength: Int
    wing: Boolean
  }

  type Query {
    animals: [Animal!]!
  }
`;

module.exports = typeDefs;
```

***resolvers.js***
```javascript=

const {Animals} = require("./tempData")

const resolvers = {
  Query: {
    animals: () => {
      return Animals;
    },
  },
};

module.exports = resolvers;
```


但我們會發現執行query後，第一筆資料的其他欄位會是null，這不是我們想要的。

![](https://i.imgur.com/hXxxMWh.png)

這時候，可以利用interface來解決上述的問題~


***typeDefs.js***
```javascript=
const { gql } = require("apollo-server");

const typeDefs = gql`
  interface Animal {
    name: String
    footLength: Int
  }

  type Spider implements Animal {
    name: String
    footLength: Int
  }

  type Bird implements Animal {
    name: String
    footLength: Int
    wingLength: Int
    wing: Boolean
  }

  type Query {
    animals: [Animal]
  }
`;

module.exports = typeDefs;
```

可以看到我們把範例資料的共同鍵值抽取出來，獨立成另外的type，這邊要注意，只要該type有implements interface，一定要把interface內的type在重新定義一次且型態都要一樣，類似抽象類別。


***resolver.js***
```javascript=
const {Animals} = require("./tempData")

const resolvers = {
  Animal: {
    __resolveType(obj) {
      //檢查obj裡面是否有wingLength的鍵值(key)
      if (obj.wingLength) {
        //有的話就回傳Bird的type
        return "Bird";
      }
        
      //沒有的話就回傳 Spider type
      return "Spider";
    },
  },

  Query: {
    animals: () => {
      return Animals;
    },
  },
};

module.exports = resolvers;
```

那query的部分要怎麼下呢?

```graphql=
query Query {
  animals {
    ... on Spider {
      name
      footLength
    }
    ... on Bird {
      name
      wingLength
      footLength
      wing
    }
  }
}
```


![](https://i.imgur.com/XRtadKz.png)


## Union

Union與Interface蠻相似的，我們可以看到以下的定義方式與Interface的差異，
最大的差異就在於如果type有implements interface，則該type必須將interface的內的type全部定義，否則會報錯，而Union則不用。

***tempData.js***
```javascript=
const Animals = [
  {
    name: "狼蛛",
    footLength: 123,
  },
  {
    name: "奇異鳥",
    wingLength: 456,
  },
];
```

***typeDefs.js***
```javascript=
const { gql } = require("apollo-server");

const typeDefs = gql`
  union Animal = Spider | Bird

  type Spider {
    name: String
    footLength: Int
  }

  type Bird {
    name: String
    wingLength: Int
  }

  type Query {
    animals: [Animal]
  }
`;

module.exports = typeDefs;
```

***resolver.js***
```javascript=
const {Animals} = require("./tempData")

const resolvers = {
  Animal: {
    __resolveType(obj) {
      if (obj.footLength) {
        return "Spider";
      }

      if (obj.wingLength) {
        return "Bird";
      }
    },
  },

  Query: {
    animals: (parent, args, context) => {
      return Animals;
    },
  },
};

module.exports = resolvers;
```

![](https://i.imgur.com/isxCIF5.png)

