https://www.jianshu.com/p/da1260b95faf
graphql 是一种用于 API 的查询语言,对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,减少数据的冗余。
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"
}
}
}
//请求
{
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 HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
// 变量
{
"ep": "JEDI"
}
// 返回数据
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
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
}
}
}
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 {
query: Query
mutation: Mutation
}
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
}
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
}
}
}
从更大的角度来看,GraphQL API 的主要应用场景是 API 网关,在客户端和服务之间提供了一个抽象层。
拥有包括移动端在内的多个客户端;
采用了微服务架构,同时希望有效管理各个服务的请求接口(中心化管理);
遗留 REST API 数量暴增,变得十分复杂;
希望消除多个客户端团队对 API 团队的依赖;
如果说grpc 面向过程的抽象,rest 面向的是资源的抽象,那么graphql 则是面向数据的抽象。所以graphql 更适合的场景是交互方更贴近数据的场景。
中台数据的一些挑战和grapqhl能够提供的优势:
丰富而异构的数据点以及挑战,对数据点的开发添加有效率上的要求
graphql 在接口设计上据有很好的可扩展性,新加的数据点不需要新添加接口endpoint,只需要添加适合的字段名。对现有的接口影响也很小。
多维度的数据模型的聚合,高度的复杂度,和服务更高耦合的接口,复杂度提升造成接口管理的困难。
多维度的数据更容易使用图的结构描述,并且可以屏蔽各个服务调用细节,使用中心化的schema 管理数据,可以更靠近字段而非以接口为管理的单元。
对应不同需求的用户调用
B端/C端 用户调用需求个有不同,graphql 统一了调用方式,不需要为不同的目的定义不同的接口调用。如果各B 端用户对接口调用的方式有需求,只需要在graphql 服务之前做一次接口转换就可以,对现有系统侵入很少。
POST 请求
{
"query": "{me{name}}",
"operationName": "...",
"variables": { "myVariable": ""}
}
响应
无论使用任何方法发送查询和变量,响应都应当以 JSON 格式在请求正文中返回。如规范中所述,查询结果可能会是一些数据和一些错误,并且应当用以下形式的 JSON 对象返回:
{
"data": { ... },
"errors": [ ... ]
}
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”}}
}
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)
可以自定义缓存策略等
Rejoiner Generates a unified GraphQL schema from gRPC microservices and other Protobuf sources
Schema 的管理去中心化,由各个微服务对外直接提供 GraphQL 请求接口,graphql service通过请求的字段名陆游到各个服务 同时将多个服务的 Schema 进行合并
优点:
缺点:
但是只找到了 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,
],
});
优点:
缺点:
缺失的版图:
由于graphql是面向数据的接口,所以架构上面必然需要有能力去描述这种图的数据模型。这样更接近本质。个人觉得目前生态中缺少一个面向数据图的服务级别的粘合器,可以中心化配置,灵活调用各种局部解析器,将整个微服务集群,从数据的角度组织成一张网络(graph)。
使用复合模式,综合多schema / 单schema 的优点:
可以通过代码或者扩展组建定制化,同时使用一些类schema (grpc protocl)代码自动生成graph schema,结合二者的数据结构。
可以中心化配置,整体对于graph 有统一的对外结构。
微服务集群需要与graphql解耦:
graphql service 不应该和微服务有过高的耦合,一些服务中间建的功能应该从graphql service移除,例如服务发现和负载均衡,流量控制等。