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 服务其实对外只提供了一个用于调用内部接口的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移除,例如服务发现和负载均衡,流量控制等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容