nodejs graphql mysql_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"

}

}

简单理解

数据结构是以一种图的形式组织的

da1260b95faf

图结构的数据

与 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 网关,在客户端和服务之间提供了一个抽象层。

da1260b95faf

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 实现

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 返回多个数据。

da1260b95faf

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 进行合并

da1260b95faf

粘合schema

优点:

schema 粘合,以此来解决开发的效率问题。对于新的数据模块(粗粒度的服务),只需要提供最新的模块的schema,解决相同类型数据的冲突,graphql service 就能够自动提供merged 之后的schema。

缺点:

每个微服务需要提供graph 接口,对接schema,使得微服务耦合了graphql 接口。

同名的类型需要解决冲突,但是解决冲突的方案可能包含业务逻辑,灵活性不是最高

粘合的功能可能还需要承载服务发现以及流量路由等功能,复杂度高,稳定性要求高

目前比较成熟的Schema Stitching方案只有基于nodejs 的,社区还不完善。

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进行组合最后返回给前端。

da1260b95faf

graphql service主动组合各个服务

优点:

对于子系统没有侵入,各个微服务和graphql 没有耦合。

graphql作为网关服务有更强的控制粒度,更加灵活,更加容易附加业务逻辑(验证,授权等)。

缺点:

接口聚集之后,如果接口频繁改动,对与graphql service 开发压力更大,流程上都依赖于graph 网关服务。

对于后端数据服务的职责划分要求更高。不宜把过重的业务逻辑放置到graphql service 中

架构想象

缺失的版图:

由于graphql是面向数据的接口,所以架构上面必然需要有能力去描述这种图的数据模型。这样更接近本质。个人觉得目前生态中缺少一个面向数据图的服务级别的粘合器,可以中心化配置,灵活调用各种局部解析器,将整个微服务集群,从数据的角度组织成一张网络(graph)。

da1260b95faf

graph technical.png

使用复合模式,综合多schema / 单schema 的优点:

可以通过代码或者扩展组建定制化,同时使用一些类schema (grpc protocl)代码自动生成graph schema,结合二者的数据结构。

可以中心化配置,整体对于graph 有统一的对外结构。

微服务集群需要与graphql解耦:

graphql service 不应该和微服务有过高的耦合,一些服务中间建的功能应该从graphql service移除,例如服务发现和负载均衡,流量控制等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值