# 2019-GraphQL-簡介
###### tags: `Tech Note` `2019` `Introduction` `GraphQL`
---
[TOC]
## REST 與 GraphQL 比較
### REST
- 簡易介紹:
:::success
表現層狀態轉換
英語:Representational State Transfer
REST是設計風格而不是標準
在2000年出現。
資源是由URI來指定。
使用HTTP協定提供的GET、POST、PUT和DELETE方法。
:::
- 優點:
- 系統撰寫靈活。
- 風格統一,直觀簡潔的URL。
- api獨立不互相影響。
- 在瀏覽器輸入URL即可獲取資料。
- 目前主流風格。
- 缺點:
- 參數、回傳值型別不固定。
- 不容易處理巢狀資源。
- 一個URL只能獲取特定資源,結構鬆散問題導致多條件查詢得創造多個URL。
- 因為無狀態,每一次只能傳輸一筆資料,無法有效的整合所需資料一次性取得。
- 會拿多餘的欄位,一次回傳的資料都為完整的結構,無法取捨不需要的資料,以至於傳輸量的增加。
- 不斷成長的 Endpoint 數量。
### GraphQL
- 簡易介紹:
:::success
GraphQL 是一種為 API 設計的資料查詢(修改)的語言
由Facebook及社區開發。
在2012年出現在2015公開釋出。
大概念上有點類似 SQL。
GraphQL 服務是通過定義類型和類型上的字段來創建的,
然後給每個類型上的每個字段提供解析函數。
:::
- 出現原因:
- 資料傳遞速度嚴重影響效能。
- 不同平台所需的資料數量、格式都不同。
- 前後端溝通難度增加。
- Legacy API 難以處理。
- 結構鬆散與查詢分割過多,導致URL過多。
- 優點:
- 精準資料取得。
- 資料只拿剛好且彈性十足。
- 前後端溝通成本減少。
- 程式即文檔。
- 前端控制權提升。
- 高度自由的實作方式。
- 不預設綁任何程式語言 (language agnostic) 或是資料庫 (DB agnostic)。
- 可將不同 micro service 的 GraphQL schema 串接在一起。
- 強型別,型別錯就直接被擋下來。
- 支援五種基礎型別 (Scalar Types)。
- 能自定義型別。
- 缺點:
- 沒有一定的實作規範,可能因為前後端對於架構的疏忽或不了解導致設計出過於複雜的 Schema。
- 仍是一種新技術(?)相關社群仍在開發中。
- 很容易一不小心陷入 RESTful API 的設計思維、埋下更多技術債。
- Server Side Caching 實作困難。
---
## 簡易介紹
:::warning
引用文章:
1. https://medium.com/@evenchange4/2018-graphql-%E6%BC%B8%E9%80%B2%E5%BC%8F%E5%B0%8E%E5%85%A5%E7%9A%84%E6%9E%B6%E6%A7%8B-aeb2603f2223
2. https://ithelp.ithome.com.tw/articles/10200678
:::
**簡單來說:**
![](https://i.imgur.com/vOI9UtV.png)
**設計:**
![](https://i.imgur.com/vbDxqDx.png)
:::success
有了 GraphQL
可以把商業模型 (business model) 像圖形 (graph) 一樣串連起來。
**Query 是 Client side 是符合 schema 規定的查詢語言格式。**
**Schema 是 Server side 定義整體資料結構格式。**
:::
---
## 資料架構(Schema)
### Schema 大概結構圖
```graphviz
digraph hierarchy {
nodesep=1.0
schema [color=Red;fontcolor=Red, fontsize=20]
QueryMain [color=Blue,fontcolor=Blue,label="Query"]
MutationMain [color=Blue,fontcolor=Blue,label="Mutation"]
schema ->QueryMain,MutationMain
QueryMain->"A function"
QueryMain->"B function"
MutationMain->"C function"
MutationMain->"D function"
"A function" -> type
"D function" -> input
"C function","B function"->{type,input}
type,input->{"Int , Float , String , Boolean , ID , scalar"}
}
```
---
### 快速簡介
:::warning
引用文章:
1. https://ithelp.ithome.com.tw/articles/10202596
:::
![](https://i.imgur.com/D3HnETu.png)
### 大致分別
- 型別 (type)
- Scalar Type
- Object Type
- 遞迴 (recursive) 取值
- 展開並選取需要的資料。
- query 結構最末端的 field 一定要是 Scalar Type。
### 範例
:::info
#### 宣告時的結構:
##### schema的整合
```
schema {
query: Query
}
```
##### Query的功能表
```
type Query {
LogIn(ID: String!, Password: String!): LogInToken!
}
```
##### 回傳的Object
```
type LogInToken {
Status: StatusData!
GetTimes: String!
AccountToken: String!
AccountID: String!
}
```
##### 在LogInToken Object 中的 Object
```
type StatusData {
StatusCode: Int!
Description: String!
}
```
:::
#### 實際請求:
```
query {
LogIn(
ID:"abcde@gmail.com",
Password:"123456789"
){
Status{
StatusCode
Description
}
GetTimes
AccountID
AccountToken
}
}
```
#### 實際回傳:
```
{
"data": {
"LogIn": {
"Status": {
"StatusCode": 2,
"Description": "Success LogIn."
},
"GetTimes": "2019-01-21 04:15:40.181350574 +0000 UTC",
"AccountID": "abcde@gmail.com",
"AccountToken": "31353035303433373931353438303434313430616263646540676d61696c2e636f6d313431313830353236302e3339383939343230373438833fca8774f34edfa7095cfb7cb05ed549d34e22be073aaa644d7ef100d10c2deb7b37fc425c7384359abd21d5a549647ae9a161a51c860d7deffe20a0b19f"
}
}
}
```
---
### 主要Schema
#### 介紹
:::info
每一個 GraphQL 服務都有一個 query 類型。
可能有一個 mutation 類型。
這兩個類型和常規對像類型無差,但是它們之所以特殊,是因為它們定義了每一個 GraphQL 查詢的『**入口**』。
**可明確分別功能所使用的schema。**
:::
#### 範例
```
schema {
query: Query
mutation: Mutation
}
```
---
### 查詢結構
#### 介紹
:::info
一個 GraphQL schema 中的最基本的組件是對像類型,它就表示你可以從服務上獲取到什麼類型的對象,以及這個對像有什麼字段。
並且有以下功能:
- 可建立多個對象類型。
- 可附加參數。
- 明確制訂回傳型態。
- 可強制非空,也可否。
- 可使用『類型』當作輸入或回傳(輸入的類型必須使用input的type,後面將會說明)。
- 可使用數組回傳。
:::
#### 範例
```
type Query {
# GraphQL 對象類型
users(
# 可附加參數(!表示『必定』為非空,沒!則否)
id:String
): [User!]! # 回傳結構([]表示數組)
post: [Post!]!
}
```
#### 備註
:::danger
如果這個參數上傳遞了一個空值(不管通過 GraphQL 字符串還是變量),那麼會導致服務器返回一個驗證錯誤。
#### 範例
##### myField: [String!]
```
有效:
myField: null
myField: []
myField: ['a', 'b']
錯誤:
myField: ['a', null, 'b']
```
##### myField: [String]!
```
有效:
myField: []
myField: ['a', 'b']
myField: ['a', null, 'b']
錯誤:
myField: null
```
:::
---
### 資料結構
#### 介紹
:::info
#### GraphQL 自帶一組默認標量類型(Scalar type)
- Int:有符號 32 位整數。
- Float:有符號雙精度浮點值。
- String:UTF‐8 字符序列。
- Boolean:true 或者 false。
- ID:ID 標量類型表示一個唯一標識符,通常用以重新獲取對像或者作為緩存中的鍵。ID 類型使用和 String 一樣的方式序列化;然而將其定義為 ID 意味著並不需要人類可讀型。
---
#### GraphQL 也可使用自定義組合類型(Object type)
這種『自定義且』、『能展開』的類型稱為Object type。
```
type A {
B:String
C:ID
D:Int
}
```
---
#### 備註
大部分的 GraphQL 服務實現中,都有『自定義標量類型』的方式。
例如,我們可以定義一個 Date 類型:
```
scalar Date
```
然後就取決於我們的實現中。
例如,你可以指定 Date 類型應該總是被序列化成整型時間戳,而客戶端應該知道去要求任何 date 字段都是這個格式。
:::
#### 範例
```
type User {
id: ID!
name: String!
number(unit: LengthUnit = METER): Float
# 回傳可附帶參數,每一個參數都必須是具名的。
# 參數可能是可選或必選,當參數為可選時,可制訂一個默認。
}
type Post {
id: ID!
title: String!
body: String!
}
```
---
#### 類型
:::success
#### 枚舉類型
枚舉類型是一種『特殊的標量』
它『**限制**』在一個特殊的『**可選值集合**』內。
驗證這個類型的任何參數是可選值的的某一個,
一個字段總是一個有限值集合的其中一個值。
下面是一個用 GraphQL schema 語言表示的 enum 定義:
```
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
```
這表示無論我們在 schema 的哪處使用了 Episode
都可以肯定它返回的是 NEWHOPE、EMPIRE 和 JEDI 之一。
:::
:::success
#### 接口類型
一個接口是一個抽像類型,它包含某些字段
而對像類型必須包含這些字段,才能算實現了這個接口。
```
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
```
這意味著任何實現 Character 的類型都要具有這些字段,並有對應參數和返回類型。
```
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
```
:::
:::success
#### 聯合類型
聯合類型和接口十分相似,但是它並不指定類型之間的任何共同字段。
任何返回一個 SearchResult 類型的地方,都可能得到一個 Human、Droid 或者 Starship。
注意,聯合類型的成員需要是具體對像類型;你不能使用接口或者其他聯合類型來創造一個聯合類型。
```
union SearchResult = Human | Droid | Starship
```
#### 範例
```
{
search(text: "an") {
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
```
:::
---
:::success
#### 輸入類型
我們只討論過將例如『枚舉』和『字符串等標量值』作為參數傳遞給字段
**但是你也能很容易地傳遞複雜對象。**
在 GraphQL schema language 中
輸入對像看上去和常規對像一模一樣,除了關鍵字是 input 而不是 type
```
input ReviewInput {
stars: Int!
commentary: String
}
```
:::
---
## 開發模式
:::warning
引用文章:
1. https://medium.com/@evenchange4/2018-graphql-%E6%BC%B8%E9%80%B2%E5%BC%8F%E5%B0%8E%E5%85%A5%E7%9A%84%E6%9E%B6%E6%A7%8B-aeb2603f2223
:::
---
### 1. REST + GraphQL Hybrid
:::info
如果直接建立一個新的GraphQL,會造成前端維護的困難,因為要同時處理 REST 與 GraphQL 的結構。
透過虛擬轉換的方式間接導入 GraphQL。
利用擴充的方式,將RESTful的部分抽象出來,加以設計。
:::
引用圖片
![](https://i.imgur.com/DEHnVEN.png)
---
### 2. GraphQL Layer
:::info
把 GraphQL Server 抽出來放到中間當作 GraphQL Layer。
既可以處裡API引用也可以直接處理DB。
:::
引用圖片
![](https://i.imgur.com/I6ActBw.png)
---
### 3. GraphQL API Gateway
:::info
GraphQL Gateway 不直接與 Database 溝通,
而是把前端的 Requests 需求往後面的 API Service 送。
而後面直接與 Database 串接的 GraphQL Server 稱為 GraphQL Native。
:::
引用圖片
![](https://i.imgur.com/cbanqmH.png)
---
## Demo
使用Framework:**99designs/gqlgen**
套件Github:https://github.com/99designs/gqlgen
文檔:https://gqlgen.com/
開發時間: 11 天 (2019/1/15 ~ 2019/1/17)
PS.包含新的DB引用、新結構的重構(完全不使用公司內的任何套件),寫資料等。
可使用數量: 2 個
可使用功能:Optend、Generalreport
### 目前架構
#### 起動流程
```graphviz
digraph hierarchy {
Start -> Config,Base
Config,Base -> Golang [label="Load", fontcolor=green]
Golang -> "Start GraphQL Gin"
Golang -> "Start RESTful Gin"
}
```
---
#### 系統流程
```graphviz
digraph hierarchy {
nodesep=1.0
Gin [color=Red;fontcolor=Red, fontsize=20]
"GraphQL API","RESTful API","GraphQL Play","Other" [color=Blue,fontcolor=Blue]
Request -> Gin -> "GraphQL API","RESTful API"[color=green]
Gin -> "GraphQL API","RESTful API","GraphQL Play","Other" [label="Run", fontcolor=red]
"GraphQL Play" -> "Data Struct" [label=" 測試用"]
"GraphQL API","RESTful API" -> Controller [label=" Examination(檢查)", color=green]
Controller -> "Return Error" [label = "不符合",color=red]
Controller -> "MySQL Database Main" [label = "符合",color=green]
}
```
```graphviz
digraph hierarchy {
nodesep=1.0
"MySQL Database Main" -> "Database Template" [label = "多工 or 等待所有資料取得",color=green]
"MySQL Database Main" ->"Extend" -> "Database Template"[label = "例外處理",color=brown]
"Database Template" -> "MySQL Database Main"[label = "處理完的資料",color=brown]
"Database Template" -> Calculation [label = "等待所有資料完成",color=green]
Calculation -> "Map to Struct" [label = "整理成結構回傳",color=green]
"Map to Struct" -> "Map to Struct Extend" [label = "特別處理",color=green]
"Map to Struct Extend" -> "Return Success Data" [label = " 無誤後回傳"]
"Return Success Data" [color=Blue, fontcolor=Red,fontsize=24,style=filled, fillcolor=green,shape=octagon]
"Return Error" [color=Blue,fontsize=24,style=filled, fillcolor=Red,shape=octagon]
"Extend","MySQL Database Main","Database Template",Calculation -> "Return Error" [label = "內部錯誤",color=red]
}
```
### 請求格式
#### URL
- http://127.0.0.1:8080/query
#### Headers
- Accept-Encoding
- gzip
- Content-Type
- application/json
- Accept
- application/json
#### Body
## Optend
### 一般
##### 結構
```
query {
OpttrEnd(Information:{
token:"12345678",
starttime:"2018-10-01",
endtime:"2018-11-01"
}){
name,
code,
data{
day,
amount,
}
}
}
```
##### 傳送內容
```
{"query":"\nquery {\nOpttrEnd(Information:{\ntoken:\"12345678\",\nstarttime:\"2018-10-01\",\nendtime:\"2018-11-01\"\n}){\nname,\ncode,\ndata{\nday,\namount,\n}\n}\n}"}
```
### 測試結果
```
{
"data": {
"OpttrEnd": [
{
"name": "入款总计",
"code": "deposit",
"data": [
{
"day": "2018-10-01T00:00:00Z",
"amount": "0"
},...
]
},
{
"name": "出款总计",
"code": "withdraw",
"data": [
{
"day": "2018-10-01T00:00:00Z",
"amount": "0"
},....
]
},
{
"name": "营利总计",
"code": "profit",
"data": [
{
"day": "2018-10-01T00:00:00Z",
"amount": "-33.53"
},....
]
}
]
}}
```
---
### 錯誤
- 時間漏打
##### 結構
```
query {
OpttrEnd(Information:{
token:"12345678",
starttime:"2018-1-01",
endtime:"2018-11-01"
}){
name,
code,
data{
day,
amount,
}
}
}
```
##### 傳送內容
```
{"query":"\nquery {\nOpttrEnd(Information:{\ntoken:\"12345678\",\nstarttime:\"2018-10-01\",\nendtime:\"2018-11-01\"\n}){\nname,\ncode,\ndata{\nday,\namount,\n}\n}\n}"}
```
### 測試結果
```
{
"errors": [
{
"message": "Time Error",
"path": [
"OpttrEnd"
]
}
],
"data": {
"OpttrEnd": null
}
}
```
## Generalreport
### 一般
##### 結構
```
query Generalreport{
Generalreport(
Information:{
token:"12345678",
starttime:"2018-10-01",
endtime:"2018-11-01"
},
Extend:{
Level:"1"
}){
ChannelName
ChannelCode
WalletCode
Others
Items{
name
level
ordercount
total
earning
point
prize
}
Pager{
Index
Pages
Size
Total
TotalOrderCount
Amount
# SubAmount
# Point
# SubPoint
# Earning
# SubEarning
# WinLose
# Income
# SubIncome
# TotalPrize
# Deposit
# SubDeposit
# Withdraw
# SubWithdraw
# Discount
# SubDiscount
# Rebate
# SubRebate
# Payout
# SubPayout
# Revenue
# Times
# SubTimes
# Charge
# SubCharge
# TotalDiscountAmount
# TotalDiscountCount
# TotalPointThreshold
# Frozen
# SubFrozen
# TotalDepositCount
# TotalWithdrawCount
# TotalDepositAmount
# TotalWithdrawAmount
# TotalTransferCharge
# TotalDepositCharge
# TotalWithdrawCharge
# TotalDepositDiscount
# RegisterCount
# LoginCount
# FirstDepositCount
# FirstDepositAmount
# FirstDepositCharge
}
}
}
```
##### 傳送內容
```
{"query":"\n\nquery Generalreport{\n Generalreport(\n Information:{\n token:\"12345678\",\n starttime:\"2018-10-01\",\n endtime:\"2018-11-01\"\n },\n Extend:{\n Level:\"1\"\n }){\n ChannelName\n ChannelCode\n WalletCode\n Others\n Items{\n name\n level\n ordercount\n total\n earning\n point\n prize\n }\n Pager{\n Index\n Pages\n Size\n Total\n TotalOrderCount\n Amount\n # SubAmount\n # Point\n # SubPoint\n # Earning\n # SubEarning\n # WinLose\n # Income\n # SubIncome\n # TotalPrize\n # Deposit\n # SubDeposit\n # Withdraw\n # SubWithdraw\n # Discount\n # SubDiscount\n # Rebate\n # SubRebate\n # Payout\n # SubPayout\n # Revenue\n # Times\n # SubTimes\n # Charge\n # SubCharge\n # TotalDiscountAmount\n # TotalDiscountCount\n # TotalPointThreshold\n # Frozen\n # SubFrozen\n # TotalDepositCount\n # TotalWithdrawCount\n # TotalDepositAmount\n # TotalWithdrawAmount\n # TotalTransferCharge\n # TotalDepositCharge\n # TotalWithdrawCharge\n # TotalDepositDiscount\n # RegisterCount\n # LoginCount\n # FirstDepositCount\n # FirstDepositAmount\n # FirstDepositCharge\n }\n }\n}\n "}
```
### 測試結果
```
{
"data": {
"Generalreport": [
{
"ChannelName": "VG-棋牌",
"ChannelCode": "vg_qipai",
"WalletCode": "vg",
"Others": "",
"Items": [
{
"name": "backendaa",
"level": "1",
"ordercount": "32",
"total": "194",
"earning": "161.55",
"point": "230.05",
"prize": "0"
},
{
"name": "ccp88888",
"level": "1",
"ordercount": "158",
"total": "246276.99",
"earning": "12964.84",
"point": "245756.39",
"prize": "0"
},
{
"name": "test11111",
"level": "1",
"ordercount": "76",
"total": "3790.18",
"earning": "809.86",
"point": "3773.42",
"prize": "0"
}
],
"Pager": {
"Index": "",
"Pages": "",
"Size": "",
"Total": "3",
"TotalOrderCount": "266",
"Amount": "250261.17"
}
},
{
"ChannelName": "福彩/体彩",
"ChannelCode": "fctc",
"WalletCode": "cp",
"Others": "",
"Items": [
{
"name": "backendaa",
"level": "1",
"ordercount": "3",
"total": "12",
"earning": "12",
"point": "12",
"prize": "0"
},
{
"name": "ccp88888",
"level": "1",
"ordercount": "30",
"total": "13170",
"earning": "13160.15",
"point": "13170",
"prize": "0"
}
],
"Pager": {
"Index": "",
"Pages": "",
"Size": "",
"Total": "2",
"TotalOrderCount": "33",
"Amount": "13182"
}
},....
```
## Golang Framework Comparison
:::warning
引用:
1. https://github.com/appleboy/golang-graphql-benchmark
:::
時間:
| Framework version | Time |
| ------------------------ | ------------------------ |
| playlyfe/go-graphql | **2018-12-03T01:16:34Z** |
| graph-gophers/graphql-go | **2017-04-28T20:40:03Z** |
| samsarahq/thunder | **2018-11-28T22:09:52Z** |
| 99designs/gqlgen | **2018-12-02T22:03:39Z** |
### Summary
| | Requests/sec |
| ----------------- | ------------ |
| graphql-go | 33448.20 |
| graph-gophers | 72506.58 |
| thunder | 71551.64 |
| gqlgen | **99645.21** |
Without graphql (only gin render json output)
| | Requests/sec |
| -------------------- | ------------- |
| json without graphql | **124663.94** |
## 其他
使用公司:https://graphql.org/users/
## 參考
1. https://ithelp.ithome.com.tw/articles/10200678
2. https://ithelp.ithome.com.tw/articles/10188294
3. https://medium.com/@evenchange4/2018-graphql-%E6%BC%B8%E9%80%B2%E5%BC%8F%E5%B0%8E%E5%85%A5%E7%9A%84%E6%9E%B6%E6%A7%8B-aeb2603f2223
4. http://graphql.cn/
5. http://graphql.org/