graphql 介紹

Graphql 介紹


graphql 是一種用于 API 的查詢語言,對你的 API 中的數(shù)據(jù)提供了一套易于理解的完整描述,使得客戶端能夠準確地獲得它需要的數(shù)據(jù)移迫,減少數(shù)據(jù)的冗余酬土。

example

  • 聲明類型
type Project {
  name: String
  tagline: String
  contributors: [User]
}
  • 查詢語句
{
  project(name: "GraphQL") {
    tagline
  }
}
  • 獲取結(jié)果
{
  "project": {
    "tagline": "A query language for APIs"
  }
}

簡單理解

  • 數(shù)據(jù)結(jié)構(gòu)是以一種圖的形式組織的


    圖結(jié)構(gòu)的數(shù)據(jù)
  • 與 RESTful 不同,每一個的 GraphQL 服務(wù)其實對外只提供了一個用于調(diào)用內(nèi)部接口的endpoint啃擦,所有的請求都訪問這個暴露出來的唯一端點囊蓝。

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

特性

請求你所要的數(shù)據(jù)
  • 可交互的查詢 客戶端請求字段珠叔,服務(wù)器根據(jù)字段返回蝎宇,哪怕是數(shù)組類的結(jié)構(gòu)依然可以根據(jù)字段名自由定制
請求
{
  hero() {
    name
    # friends 表示數(shù)組
    friends {
      name
    }
  }
}

返回
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}
  • 使用參數(shù)查詢
// 請求
{
  human(id: "1000") {
    name
  }
}
// 返回
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}
  • 使用別名
    有的時候希望在一次請求過程中,對同一個字段使用不同的參數(shù)做兩次請求
// 請求hero字段兩次祷安,使用不同的參數(shù)
{
    empireHero: hero(episode: EMPIRE) {
      name
    }
    jediHero: hero(episode: JEDI) {
      name
    }
}

// 返回
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}
  • 片段(Fragments)
    片段使你能夠組織一組字段姥芥,然后在需要它們的的地方引入,達到復(fù)用單元的意義汇鞭。
//請求
{
  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凉唐,通過提交不同的變量來實現(xiàn)
// 查詢語句
query Hero($episode: Episode) {
  hero(episode: $episode) {
    name
  }
}
// 變量
{
  "episode": "JEDI"
}

// 返回數(shù)據(jù)
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}
  • 內(nèi)聯(lián)數(shù)據(jù)塊
    如果查詢的字段返回的是接口或者聯(lián)合類型庸追,那么你可能需要使用內(nèi)聯(lián)片段來取出下層具體類型的數(shù)據(jù):
// 查詢語句
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
// 變量
{
  "ep": "JEDI"
}
// 返回數(shù)據(jù)
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}
  • 變更(Mutations)
    不只是查詢,還能夠變更數(shù)據(jù)
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

// 變量
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

//返回結(jié)果
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

// 完整的query 寫法
// query 是操作類型 query mutation subscription
// HeroNameAndFriends 是操作名稱
query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}
類型系統(tǒng) (schema)

example:

// schema 文件入口
schema {
  query: Query
  mutation: Mutation
}
// query 操作聲明
type Query {
  // 參數(shù)熊榛,聲明該字段能夠接受的參數(shù)
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}
// 枚舉類型
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

//對象類型和字段
type Character {
  //! 符號用于表示該字段非空
  name: String!
  appearsIn: [Episode]! // 字段類型是一個數(shù)組
}

// 接口類型
interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

// 實現(xiàn)特殊的接口
type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

// 實現(xiàn)特殊的接口
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 {
  // 參數(shù)锚国,聲明該字段能夠接受的參數(shù)
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}
  • 枚舉類型
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

  • 對象類型和字段
type Character {
  //! 符號用于表示該字段非空
  name: String!
  appearsIn: [Episode]! // 字段類型是一個數(shù)組
}
  • 參數(shù)
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
}
  • 實現(xiàn)特殊的接口的對象類型
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 的主要應(yīng)用場景是 API 網(wǎng)關(guān)玄坦,在客戶端和服務(wù)之間提供了一個抽象層血筑。

image
  • 擁有包括移動端在內(nèi)的多個客戶端;

  • 采用了微服務(wù)架構(gòu)煎楣,同時希望有效管理各個服務(wù)的請求接口(中心化管理)豺总;

  • 遺留 REST API 數(shù)量暴增,變得十分復(fù)雜择懂;

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

如果說grpc 面向過程的抽象,rest 面向的是資源的抽象困曙,那么graphql 則是面向數(shù)據(jù)的抽象表伦。所以graphql 更適合的場景是交互方更貼近數(shù)據(jù)的場景。

數(shù)據(jù)中臺與graphql

中臺數(shù)據(jù)的一些挑戰(zhàn)和grapqhl能夠提供的優(yōu)勢:

  • 豐富而異構(gòu)的數(shù)據(jù)點以及挑戰(zhàn)慷丽,對數(shù)據(jù)點的開發(fā)添加有效率上的要求
    graphql 在接口設(shè)計上據(jù)有很好的可擴展性蹦哼,新加的數(shù)據(jù)點不需要新添加接口endpoint,只需要添加適合的字段名要糊。對現(xiàn)有的接口影響也很小纲熏。

  • 多維度的數(shù)據(jù)模型的聚合,高度的復(fù)雜度锄俄,和服務(wù)更高耦合的接口局劲,復(fù)雜度提升造成接口管理的困難。
    多維度的數(shù)據(jù)更容易使用圖的結(jié)構(gòu)描述奶赠,并且可以屏蔽各個服務(wù)調(diào)用細節(jié)鱼填,使用中心化的schema 管理數(shù)據(jù),可以更靠近字段而非以接口為管理的單元车柠。

  • 對應(yīng)不同需求的用戶調(diào)用
    B端/C端 用戶調(diào)用需求個有不同剔氏,graphql 統(tǒng)一了調(diào)用方式,不需要為不同的目的定義不同的接口調(diào)用竹祷。如果各B 端用戶對接口調(diào)用的方式有需求,只需要在graphql 服務(wù)之前做一次接口轉(zhuǎn)換就可以羊苟,對現(xiàn)有系統(tǒng)侵入很少塑陵。

應(yīng)用方案

通過 HTTP 提供服務(wù)
  • POST 請求
    {
    "query": "{me{name}}",
    "operationName": "...",
    "variables": { "myVariable": ""}
    }

  • 響應(yīng)
    無論使用任何方法發(fā)送查詢和變量,響應(yīng)都應(yīng)當以 JSON 格式在請求正文中返回蜡励。如規(guī)范中所述令花,查詢結(jié)果可能會是一些數(shù)據(jù)和一些錯誤阻桅,并且應(yīng)當用以下形式的 JSON 對象返回:
    {
    "data": { ... },
    "errors": [ ... ]
    }

graphql 實現(xiàn)

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 作為的網(wǎng)關(guān)特點,在一次請求中可能會訪問多個服務(wù)兼都,在沒有優(yōu)化的情況下嫂沉,往往會發(fā)送多個請求給后臺服務(wù)。造成性能浪費

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

解決方案 DataLoader
DataLoader被廣泛地應(yīng)用于解決[N+1查詢問題]

對于多個相同類別的數(shù)據(jù)使用同一個請求扮碧,傳入多個id 返回多個數(shù)據(jù)趟章。


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}`));

緩存
內(nèi)存級別的緩存,load一次慎王,DataLoader就會把數(shù)據(jù)緩存在內(nèi)存蚓土,下一次再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

架構(gòu)方案 schema 中心化/多版本

  • 多版本調(diào)用

Schema 的管理去中心化蜀漆,由各個微服務(wù)對外直接提供 GraphQL 請求接口,graphql service通過請求的字段名陸游到各個服務(wù) 同時將多個服務(wù)的 Schema 進行合并


粘合schema

優(yōu)點:

  • schema 粘合咱旱,以此來解決開發(fā)的效率問題确丢。對于新的數(shù)據(jù)模塊(粗粒度的服務(wù)),只需要提供最新的模塊的schema吐限,解決相同類型數(shù)據(jù)的沖突鲜侥,graphql service 就能夠自動提供merged 之后的schema。

缺點:

  • 每個微服務(wù)需要提供graph 接口毯盈,對接schema剃毒,使得微服務(wù)耦合了graphql 接口。
  • 同名的類型需要解決沖突搂赋,但是解決沖突的方案可能包含業(yè)務(wù)邏輯赘阀,靈活性不是最高
  • 粘合的功能可能還需要承載服務(wù)發(fā)現(xiàn)以及流量路由等功能,復(fù)雜度高脑奠,穩(wěn)定性要求高
  • 目前比較成熟的Schema Stitching方案只有基于nodejs 的基公,社區(qū)還不完善。

但是只找到了 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,
  ],
});
  • 中心化調(diào)用
    一個中心化的schema和graphql service宋欺,各個微服務(wù)提供rpc 接口或者rest api接口轰豆,graphql service主動調(diào)用別的微服務(wù)rpc 接口,按照schema進行組合最后返回給前端齿诞。
graphql service主動組合各個服務(wù)

優(yōu)點:

  • 對于子系統(tǒng)沒有侵入酸休,各個微服務(wù)和graphql 沒有耦合。
  • graphql作為網(wǎng)關(guān)服務(wù)有更強的控制粒度祷杈,更加靈活斑司,更加容易附加業(yè)務(wù)邏輯(驗證,授權(quán)等)但汞。

缺點:

  • 接口聚集之后宿刮,如果接口頻繁改動互站,對與graphql service 開發(fā)壓力更大,流程上都依賴于graph 網(wǎng)關(guān)服務(wù)僵缺。
  • 對于后端數(shù)據(jù)服務(wù)的職責劃分要求更高胡桃。不宜把過重的業(yè)務(wù)邏輯放置到graphql service 中

架構(gòu)想象

缺失的版圖:
由于graphql是面向數(shù)據(jù)的接口,所以架構(gòu)上面必然需要有能力去描述這種圖的數(shù)據(jù)模型磕潮。這樣更接近本質(zhì)翠胰。個人覺得目前生態(tài)中缺少一個面向數(shù)據(jù)圖的服務(wù)級別的粘合器,可以中心化配置揉抵,靈活調(diào)用各種局部解析器亡容,將整個微服務(wù)集群,從數(shù)據(jù)的角度組織成一張網(wǎng)絡(luò)(graph)冤今。


graph technical.png

使用復(fù)合模式闺兢,綜合多schema / 單schema 的優(yōu)點:
可以通過代碼或者擴展組建定制化,同時使用一些類schema (grpc protocl)代碼自動生成graph schema戏罢,結(jié)合二者的數(shù)據(jù)結(jié)構(gòu)屋谭。
可以中心化配置,整體對于graph 有統(tǒng)一的對外結(jié)構(gòu)龟糕。

微服務(wù)集群需要與graphql解耦:
graphql service 不應(yīng)該和微服務(wù)有過高的耦合桐磁,一些服務(wù)中間建的功能應(yīng)該從graphql service移除,例如服務(wù)發(fā)現(xiàn)和負載均衡讲岁,流量控制等我擂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缓艳,隨后出現(xiàn)的幾起案子校摩,更是在濱河造成了極大的恐慌,老刑警劉巖阶淘,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衙吩,死亡現(xiàn)場離奇詭異,居然都是意外死亡溪窒,警方通過查閱死者的電腦和手機坤塞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澈蚌,“玉大人摹芙,你說我怎么就攤上這事⊥鹈椋” “怎么了瘫辩?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長坛悉。 經(jīng)常有香客問我伐厌,道長,這世上最難降的妖魔是什么裸影? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任挣轨,我火速辦了婚禮,結(jié)果婚禮上轩猩,老公的妹妹穿的比我還像新娘卷扮。我一直安慰自己,他們只是感情好均践,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布晤锹。 她就那樣靜靜地躺著,像睡著了一般彤委。 火紅的嫁衣襯著肌膚如雪鞭铆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天焦影,我揣著相機與錄音车遂,去河邊找鬼。 笑死斯辰,一個胖子當著我的面吹牛舶担,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播彬呻,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼衣陶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闸氮?” 一聲冷哼從身側(cè)響起剪况,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎湖苞,沒想到半個月后拯欧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡财骨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年镐作,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隆箩。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡该贾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出捌臊,到底是詐尸還是另有隱情杨蛋,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站逞力,受9級特大地震影響曙寡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寇荧,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一举庶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧揩抡,春花似錦户侥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至烁设,卻和暖如春替梨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背署尤。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工耙替, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人曹体。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓俗扇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親箕别。 傳聞我的和親對象是個殘疾皇子铜幽,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容