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
访问其他资源的school
和teacher
等资源的图,多个请求变成了一个请求的不同字段,从原有的分散式请求变成了集中式的请求。
特性
请求你所要的数据
- 可交互的查询 客户端请求字段,服务器根据字段返回,哪怕是数组类的结构依然可以根据字段名自由定制
请求 { 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 网关,在客户端和服务之间提供了一个抽象层。
-
拥有包括移动端在内的多个客户端;
-
采用了微服务架构,同时希望有效管理各个服务的请求接口(中心化管理);
-
遗留 REST API 数量暴增,变得十分复杂;
-
希望消除多个客户端团队对 API 团队的依赖;
如果说grpc 面向过程的抽象,rest 面向的是资源的抽象,那么graphql 则是面向数据的抽象。所以graphql 更适合的场景是交互方更贴近数据的场景。
数据中台与graphql
中台数据的一些挑战和grapqhl能够提供的优势:
-
丰富而异构的数据点以及挑战,对数据点的开发添加有效率上的要求
graphql 在接口设计上据有很好的可扩展性,新加的数据点不需要新添加接口endpoint,只需要添加适合的字段名。对现有的接口影响也很小。 -
多维度的数据模型的聚合,高度的复杂度,和服务更高耦合的接口,复杂度提升造成接口管理的困难。
多维度的数据更容易使用图的结构描述,并且可以屏蔽各个服务调用细节,使用中心化的schema 管理数据,可以更靠近字段而非以接口为管理的单元。 -
对应不同需求的用户调用
B端/C端 用户调用需求个有不同,graphql 统一了调用方式,不需要为不同的目的定义不同的接口调用。如果各B 端用户对接口调用的方式有需求,只需要在graphql 服务之前做一次接口转换就可以,对现有系统侵入很少。
应用方案
通过 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 ", rJSON) // {“data”:{“hello”:”world”}} }
N+1 问题
graphql 作为的网关特点,在一次请求中可能会访问多个服务,在没有优化的情况下,往往会发送多个请求给后台服务。造成性能浪费
{ school { students { // n student ..... } } }
解决方案 DataLoader
DataLoader被广泛地应用于解决[N+1查询问题]对于多个相同类别的数据使用同一个请求,传入多个id 返回多个数据。
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,解决相同类型数据的冲突,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 没有耦合。
- graphql作为网关服务有更强的控制粒度,更加灵活,更加容易附加业务逻辑(验证,授权等)。
缺点:
- 接口聚集之后,如果接口频繁改动,对与graphql service 开发压力更大,流程上都依赖于graph 网关服务。
- 对于后端数据服务的职责划分要求更高。不宜把过重的业务逻辑放置到graphql service 中
架构想象
缺失的版图:
由于graphql是面向数据的接口,所以架构上面必然需要有能力去描述这种图的数据模型。这样更接近本质。个人觉得目前生态中缺少一个面向数据图的服务级别的粘合器,可以中心化配置,灵活调用各种局部解析器,将整个微服务集群,从数据的角度组织成一张网络(graph)。
使用复合模式,综合多schema / 单schema 的优点:
可以通过代码或者扩展组建定制化,同时使用一些类schema (grpc protocl)代码自动生成graph schema,结合二者的数据结构。
可以中心化配置,整体对于graph 有统一的对外结构。微服务集群需要与graphql解耦:
graphql service 不应该和微服务有过高的耦合,一些服务中间建的功能应该从graphql service移除,例如服务发现和负载均衡,流量控制等。
作者:xuyaozuo
链接:https://www.jianshu.com/p/da1260b95faf
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 -