graphql 介紹
Graphql 介紹
graphql 是一種用於 API 的查詢語言,對你的 API 中的資料提供了一套易於理解的完整描述,使得客戶端能夠準確地獲得它需要的資料,減少資料的冗餘。
example
- 宣告型別
type Project { name: String tagline: String contributors: [User] }
- 查詢語句
{ project(name: "GraphQL") { tagline } }
- 獲取結果
{ "project": { "tagline": "A query language for APIs" } }
理解
-
資料結構是以一種圖的形式組織的
圖結構的資料
-
與 RESTful 不同,每一個的 GraphQL 服務其實對外只提供了一個用於呼叫內部介面的端點,所有的請求都訪問這個暴露出來的唯一端點。
-
GraphQL 實際上將多個 HTTP 請求聚合成了一個請求,它只是將多個 RESTful 請求的資源變成了一個從根資源
Post
訪問其他資源的Comment
和Author
的圖,多個請求變成了一個請求的不同欄位,從原有的分散式請求變成了集中式的請求。
適用場景
從更大的角度來看,GraphQL API 的主要應用場景是 API 閘道器,在客戶端和服務之間提供了一個抽象層。

image
-
擁有包括移動端在內的多個客戶端;
-
正在轉向或者已經採用了微服務架構;
-
遺留 REST API 數量暴增,變得十分複雜;
-
希望消除客戶端團隊對 API 團隊的依賴;
特性
請求你所要的資料
- 可互動的查詢 客戶端請求欄位,伺服器根據欄位返回
請求 { hero(id: "1000") { name # friends 表示陣列 friends { name } } } 返回 { "data": { "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] } } }
- 使用引數查詢
// 請求 { human(id: "1000") { name height(unit: FOOT) } } // 返回 { "data": { "human": { "name": "Luke Skywalker", "height": 5.6430448 } } }
- 使用別名
// 請求 { empireHero: hero(episode: EMPIRE) { name } jediHero: hero(episode: JEDI) { name } } // 返回 { "data": { "empireHero": { "name": "Luke Skywalker" }, "jediHero": { "name": "R2-D2" } } }
- 片段(Fragments)
//請求 { leftComparison: hero(episode: EMPIRE) { ...comparisonFields } rightComparison: hero(episode: JEDI) { ...comparisonFields } } fragment comparisonFields on Character { name appearsIn friends { name } } // 返回 { "data": { "leftComparison": { "name": "Luke Skywalker", "appearsIn": [ "NEWHOPE", "EMPIRE", "JEDI" ], "friends": [ { "name": "Han Solo" }, { "name": "Leia Organa" }, { "name": "C-3PO" }, { "name": "R2-D2" } ] }, "rightComparison": { "name": "R2-D2", "appearsIn": [ "NEWHOPE", "EMPIRE", "JEDI" ], "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] } } }
- 變數
// 查詢語句 query Hero($episode: Episode) { hero(episode: $episode) { name } } // 變數 { "episode": "JEDI" } // 返回資料 { "data": { "hero": { "name": "R2-D2" } } }
- 內聯資料塊
// 查詢語句 query Hero($episode: Episode) { hero(episode: $episode) { name } } // 變數 { "episode": "JEDI" } // 返回資料 { "data": { "hero": { "name": "R2-D2" } } }
-
變更(Mutations)
不只是查詢,還能夠變更資料
// query 是操作型別 query mutation subscription // HeroNameAndFriends 是操作名稱 query HeroNameAndFriends { hero { name friends { name } } }
- 放一起
{ hero(id: "1000") { name # 註釋 friends { name } } empireHero: hero(episode: EMPIRE) { name } jediHero: hero(episode: JEDI) { name } leftComparison: hero(episode: EMPIRE) { ...comparisonFields } rightComparison: hero(episode: JEDI) { ...comparisonFields } } fragment comparisonFields on Character { name appearsIn friends { name } }
response:
{ "data": { "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] }, "empireHero": { "name": "Luke Skywalker" }, "jediHero": { "name": "R2-D2" }, "leftComparison": { "name": "Luke Skywalker", "appearsIn": [ "NEWHOPE", "EMPIRE", "JEDI" ], "friends": [ { "name": "Han Solo" }, { "name": "Leia Organa" }, { "name": "C-3PO" }, { "name": "R2-D2" } ] }, "rightComparison": { "name": "R2-D2", "appearsIn": [ "NEWHOPE", "EMPIRE", "JEDI" ], "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] } } }
型別系統
// schema 檔案入口 schema { query: Query mutation: Mutation } // query 操作宣告 type Query { // 引數,宣告該欄位能夠接受的引數 hero(episode: Episode): Character droid(id: ID!): Droid } // 列舉型別 enum Episode { NEWHOPE EMPIRE JEDI } //物件型別和欄位 type Character { //! 符號用於表示該欄位非空 name: String! appearsIn: [Episode]! // 欄位型別是一個數組 } // 介面型別 interface Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! } // 實現特殊的介面 type Human implements Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! starships: [Starship] totalCredits: Int } // 實現特殊的介面 type Droid implements Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! primaryFunction: String } input ReviewInput { stars: Int! commentary: String }
// 使用interface 型別 進行查詢 query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { name ... on Droid { primaryFunction } } }
應用方案
通過 HTTP 提供服務
-
GET 請求
url: http://myapi/graphql?query= {me{name}}& var.name=
-
POST 請求
{
"query": "{me{name}}",
"operationName": "...",
"variables": { "myVariable": "
-
響應
無論使用任何方法傳送查詢和變數,響應都應當以 JSON 格式在請求正文中返回。如規範中所述,查詢結果可能會是一些資料和一些錯誤,並且應當用以下形式的 JSON 物件返回:
{
"data": { ... },
"errors": [ ... ]
}
graphql 實現
golang github.com/graphql-go/graphql
func main() { // Schema fields := graphql.Fields{ "hello": &graphql.Field{ Type: graphql.String, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return "world", nil }, }, } rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields} schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} schema, err := graphql.NewSchema(schemaConfig) if err != nil { log.Fatalf("failed to create new schema, error: %v", err) } // Query query := ` { hello } ` params := graphql.Params{Schema: schema, RequestString: query} r := graphql.Do(params) if len(r.Errors) > 0 { log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors) } rJSON, _ := json.Marshal(r) fmt.Printf("%s \n", rJSON) // {“data”:{“hello”:”world”}} }
DataLoader
DataLoader被廣泛地應用於解決[N+1查詢問題]
-
批處理 (N+1 問題)
對於多個相同類別的資料使用同一個請求,傳入多個id 返回多個數據。
image.png
var DataLoader = require('dataloader') var userLoader = new DataLoader(keys => myBatchGetUsers(keys)); userLoader.load(1) .then(user => userLoader.load(user.invitedByID)) .then(invitedBy => console.log(`User 1 was invited by ${invitedBy}`)); // Elsewhere in your application userLoader.load(2) .then(user => userLoader.load(user.lastInvitedID)) .then(lastInvited => console.log(`User 2 last invited ${lastInvited}`));
-
快取
記憶體級別的快取,load一次,DataLoader就會把資料快取在記憶體,下一次再load時,就不會再去訪問後臺。
var userLoader = new DataLoader(...) var promise1A = userLoader.load(1) var promise1B = userLoader.load(1) assert(promise1A === promise1B)
可以自定義快取策略等
gprc 與 graphql (java)
Rejoiner Generates a unified GraphQL schema from gRPC microservices and other Protobuf sources
schema 中心化/多版本
- 多版本呼叫
Schema 的管理去中心化,由各個微服務對外直接提供 GraphQL 請求介面,graphql service通過請求的欄位名陸游到各個服務 同時將多個服務的 Schema 進行合併

粘合schema
優點:
- schema 粘合,以此來解決開發的效率問題。
缺點:
- 每個微服務需要提供graph 介面,對接schema,使得微服務耦合了graphql 介面。
- 粘合的功能可能還需要承載服務發現以及流量路由等功能,複雜度高,穩定性要求高
- 目前比較成熟的Schema Stitching方案只有基於nodejs 的,社群還不完善。
-
中心化呼叫
一箇中心化的schema和graphql service,各個微服務提供rpc 介面或者rest api介面,graphql service主動呼叫別的微服務rpc 介面,按照schema進行組合最後返回給前端。

graphql service主動組合各個服務
優點:
- 對於子系統沒有侵入,各個微服務和graphql 沒有耦合。
- graphql作為閘道器服務有更強的控制粒度,更加靈活,更加容易附加業務邏輯(驗證,授權等)。
缺點:
- 介面聚集之後,如果介面頻繁改動,對與graphql service 開發壓力更大,流程上都依賴於graph 閘道器服務。
- 對於後端資料服務的職責劃分要求更高。不宜把過重的業務邏輯放置到graphql service 中
架構想象
使用複合模式,綜合多schema / 單schema 的優點:
可以通過程式碼或者外掛定製化,同時使用一些類schema (grpc protocl)程式碼自動生成graph schema,結合二者的資料結構。
可以中心化配置,整體對於graph 有統一的對外結構。
微服務叢集需要與graphql解耦:
graphql service 不應該和微服務有過高的耦合,一些服務中間建的功能應該從graphql service,例如服務發現和負載均衡等。
缺失的版圖:
缺少一個面向圖的服務級別的粘合器,可以中心化配置,靈活呼叫各種區域性解析器,將整個微服務叢集,從資料的角度組織成一張網路。

graph technical.png
資料中臺與graphql
中臺資料的一些挑戰和grapqhl能夠提供的優勢:
-
豐富而異構的資料點以及挑戰,對資料點的開發新增有效率上的要求
graphql 在介面設計上據有很好的可擴充套件性,新加的資料點不需要新新增介面endpoint,只需要新增適合的欄位名。對現有的介面影響也很小。
-
多維度的資料模型的聚合,高度的複雜度,和服務更高耦合的介面,複雜度提升造成介面管理的困難。
多維度的資料更容易使用圖的結構描述,並且可以遮蔽各個服務呼叫細節,使用中心化的schema 管理資料,可以更靠近欄位而非以介面為管理的單元。
-
對應不用需求的使用者呼叫
B端/C端 使用者呼叫需求個有不同,graphql 統一了呼叫方式,不需要為不同的目的定義不同的介面呼叫。如果各B 端使用者對介面呼叫的方式有需求,只需要在graphql 服務之前做一次介面轉換就可以,對現有系統侵入很少。