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 服務其實對外只提供了一個用於調用內部接口的endpoint,所有的請求都訪問這個暴露出來的唯一端點。

    • GraphQL 實際上將多個 HTTP 請求聚合成了一個請求,它只是將多個 RESTful 請求的資源變成了一個從根資源 Post 訪問其他資源的 schoolteacher等資源的圖,多個請求變成了一個請求的不同字段,從原有的分散式請求變成了集中式的請求。

    特性

    請求你所要的數據
    • 可交互的查詢 客戶端請求字段,服務器根據字段返回,哪怕是數組類的結構依然可以根據字段名自由定制
    請求
    {
      hero() {
        name
        # friends 表示數組
        friends {
          name
        }
      }
    }
    
    返回
    {
      "data": {
        "hero": {
          "name": "R2-D2",
          "friends": [
            {
              "name": "Luke Skywalker"
            },
            {
              "name": "Han Solo"
            },
            {
              "name": "Leia Organa"
            }
          ]
        }
      }
    }
    
    • 使用參數查詢
    // 請求 { human(id: "1000") { name } } // 返回 { "data": { "human": { "name": "Luke Skywalker", "height": 5.6430448 } } } 
    • 使用別名
      有的時候希望在一次請求過程中,對同一個字段使用不同的參數做兩次請求
    // 請求hero字段兩次,使用不同的參數 { 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,通過提交不同的變量來實現
    // 查詢語句 query Hero($episode: Episode) { hero(episode: $episode) { name } } // 變量 { "episode": "JEDI" } // 返回數據 { "data": { "hero": { "name": "R2-D2" } } } 
    • 內聯數據塊
      如果查詢的字段返回的是接口或者聯合類型,那么你可能需要使用內聯片段來取出下層具體類型的數據:
    // 查詢語句 query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { name ... on Droid { primaryFunction } ... on Human { height } } } // 變量 { "ep": "JEDI" } // 返回數據 { "data": { "hero": { "name": "R2-D2", "primaryFunction": "Astromech" } } } 
    • 變更(Mutations)
      不只是查詢,還能夠變更數據
    mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { stars commentary } } // 變量 { "ep": "JEDI", "review": { "stars": 5, "commentary": "This is a great movie!" } } //返回結果 { "data": { "createReview": { "stars": 5, "commentary": "This is a great movie!" } } } // 完整的query 寫法 // query 是操作類型 query mutation subscription // HeroNameAndFriends 是操作名稱 query HeroNameAndFriends { hero { name friends { name } } } 
    類型系統 (schema)

    example:

    // 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 } 
    • 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]! // 字段類型是一個數組 } 
    • 參數
    type Starship { id: ID! name: String! length(unit: LengthUnit = METER): Float // 可以使用默認值 } 
    • 接口類型
    interface Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! } 
    • 輸入類型
    input ReviewInput { stars: Int! commentary: String } 
    • 實現特殊的接口的對象類型
    type Human implements Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! starships: [Starship] totalCredits: Int } 
    • 基於接口類型的查找類型
      使用interface 類型 進行查找
    query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { name ... on Droid { primaryFunction } ... on Human { } } } 

    適用場景

    從更大的角度來看,GraphQL API 的主要應用場景是 API 網關,在客戶端和服務之間提供了一個抽象層。

     
    image
    • 擁有包括移動端在內的多個客戶端;

    • 采用了微服務架構,同時希望有效管理各個服務的請求接口(中心化管理);

    • 遺留 REST API 數量暴增,變得十分復雜;

    • 希望消除多個客戶端團隊對 API 團隊的依賴;

    如果說grpc 面向過程的抽象,rest 面向的是資源的抽象,那么graphql 則是面向數據的抽象。所以graphql 更適合的場景是交互方更貼近數據的場景。

    數據中台與graphql

    中台數據的一些挑戰和grapqhl能夠提供的優勢:

    • 豐富而異構的數據點以及挑戰,對數據點的開發添加有效率上的要求
      graphql 在接口設計上據有很好的可擴展性,新加的數據點不需要新添加接口endpoint,只需要添加適合的字段名。對現有的接口影響也很小。

    • 多維度的數據模型的聚合,高度的復雜度,和服務更高耦合的接口,復雜度提升造成接口管理的困難。
      多維度的數據更容易使用圖的結構描述,並且可以屏蔽各個服務調用細節,使用中心化的schema 管理數據,可以更靠近字段而非以接口為管理的單元。

    • 對應不同需求的用戶調用
      B端/C端 用戶調用需求個有不同,graphql 統一了調用方式,不需要為不同的目的定義不同的接口調用。如果各B 端用戶對接口調用的方式有需求,只需要在graphql 服務之前做一次接口轉換就可以,對現有系統侵入很少。

    應用方案

    通過 HTTP 提供服務
    • 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”}} } 

    N+1 問題

    graphql 作為的網關特點,在一次請求中可能會訪問多個服務,在沒有優化的情況下,往往會發送多個請求給后台服務。造成性能浪費

    { school { students { // n student ..... } } } 

    解決方案 DataLoader
    DataLoader被廣泛地應用於解決[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 粘合,以此來解決開發的效率問題。對於新的數據模塊(粗粒度的服務),只需要提供最新的模塊的schema,解決相同類型數據的沖突,graphql service 就能夠自動提供merged 之后的schema。

    缺點:

    • 每個微服務需要提供graph 接口,對接schema,使得微服務耦合了graphql 接口。
    • 同名的類型需要解決沖突,但是解決沖突的方案可能包含業務邏輯,靈活性不是最高
    • 粘合的功能可能還需要承載服務發現以及流量路由等功能,復雜度高,穩定性要求高
    • 目前比較成熟的Schema Stitching方案只有基於nodejs 的,社區還不完善。

    但是只找到了 javascript 解決方案

    import { makeExecutableSchema, addMockFunctionsToSchema, mergeSchemas, } from 'graphql-tools'; // Mocked chirp schema // We don't worry about the schema implementation right now since we're just // demonstrating schema stitching. const chirpSchema = makeExecutableSchema({ typeDefs: ` type Chirp { id: ID! text: String authorId: ID! } type Query { chirpById(id: ID!): Chirp chirpsByAuthorId(authorId: ID!): [Chirp] } ` }); addMockFunctionsToSchema({ schema: chirpSchema }); // Mocked author schema const authorSchema = makeExecutableSchema({ typeDefs: ` type User { id: ID! email: String } type Query { userById(id: ID!): User } ` }); addMockFunctionsToSchema({ schema: authorSchema }); export const schema = mergeSchemas({ schemas: [ chirpSchema, authorSchema, ], }); 
    • 中心化調用
      一個中心化的schema和graphql service,各個微服務提供rpc 接口或者rest api接口,graphql service主動調用別的微服務rpc 接口,按照schema進行組合最后返回給前端。
     
    graphql service主動組合各個服務

    優點:

    • 對於子系統沒有侵入,各個微服務和graphql 沒有耦合。
    • graphql作為網關服務有更強的控制粒度,更加靈活,更加容易附加業務邏輯(驗證,授權等)。

    缺點:

    • 接口聚集之后,如果接口頻繁改動,對與graphql service 開發壓力更大,流程上都依賴於graph 網關服務。
    • 對於后端數據服務的職責划分要求更高。不宜把過重的業務邏輯放置到graphql service 中

    架構想象

    缺失的版圖:
    由於graphql是面向數據的接口,所以架構上面必然需要有能力去描述這種圖的數據模型。這樣更接近本質。個人覺得目前生態中缺少一個面向數據圖的服務級別的粘合器,可以中心化配置,靈活調用各種局部解析器,將整個微服務集群,從數據的角度組織成一張網絡(graph)。


     
    graph technical.png

    使用復合模式,綜合多schema / 單schema 的優點:
    可以通過代碼或者擴展組建定制化,同時使用一些類schema (grpc protocl)代碼自動生成graph schema,結合二者的數據結構。
    可以中心化配置,整體對於graph 有統一的對外結構。

    微服務集群需要與graphql解耦:
    graphql service 不應該和微服務有過高的耦合,一些服務中間建的功能應該從graphql service移除,例如服務發現和負載均衡,流量控制等。

     
     
    3人點贊
     
    coding
     
     


    作者:xuyaozuo
    鏈接:https://www.jianshu.com/p/da1260b95faf
    來源:簡書
    著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM