# 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/