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)是以一種圖的形式組織的
與 RESTful 不同,每一個的 GraphQL 服務(wù)其實對外只提供了一個用于調(diào)用內(nèi)部接口的endpoint啃擦,所有的請求都訪問這個暴露出來的唯一端點囊蓝。
GraphQL 實際上將多個 HTTP 請求聚合成了一個請求,它只是將多個 RESTful 請求的資源變成了一個從根資源
Post
訪問其他資源的school
和teacher
等資源的圖令蛉,多個請求變成了一個請求的不同字段聚霜,從原有的分散式請求變成了集中式的請求。
特性
請求你所要的數(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ù)之間提供了一個抽象層血筑。
擁有包括移動端在內(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ù)
- GET 請求
url: http://myapi/graphql?query={me{name}}&var.name=
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ù)趟章。
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 進行合并
優(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進行組合最后返回給前端齿诞。
優(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)冤今。
使用復(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)和負載均衡讲岁,流量控制等我擂。