什麼是 GraphQL?為什麼要用 GraphQL?

GraphQL - A Query Language for APIs

GraphQL is a new API standard that provides a more efficient, powerful and flexible alternative to REST. It was developed and open-sourced by Facebook and is now maintained by a large community of companies and individuals from all over the world. At its core, GraphQL enables declarative data fetching where a client can specify exactly what data it needs from an API. Instead of multiple endpoints that return fixed data structures, a GraphQL server only exposes a single endpoint and responds with precisely the data a client asked for.

這是官方關於 GraphQL 的介紹,濃縮成一句話解釋: an API defines how a client can load data from a server

大概可以達到這些優點:

① It lets the client specify exactly what data it needs. / 由前端決定需要哪些資料
② It uses a type system to describe data. / 資料是具有嚴謹的型態規範
③ It makes it easier to aggregate data from multiple sources. / 只需要單一的 API 端口

GraphQL is the better REST

跟傳統的 REST 的相比,可以用這張圖解釋:

根據 REST 的做法,我們依照每一個 Resource 為單位(通常 depend on 資料庫的表格)。也就是說,前端需要一個資源時即需要呼叫一次 API,N 個資源時需要呼叫 N 次。可能會隱性的增加 Request 與增加新資源的開發成本。 RESTFul 分為幾個階段:Request → URL Route → Handle Controller -> Multiple Endpoint

在 GraphQL 的架構中將 URL Route → Handle Controller 的動作直接壓縮設計在 API 中透過 Schema 跟 Resolver 取代,將 Resources 視為是一個 Graph,僅需要透過 One Endpoint API 就可以做到對資料存取的操作。像這樣使用:

為什麼要用 GraphQL?

目前應用程式的發展是很快速的,資料的定義也很難在一開始就設定得很完美,很常都是前、後端同時開發同時串接。而因為 API 的興起,一個後端通常需要應付很多前端平台,像是網頁前、後台、Android、iOS app 之類的。多個呈現畫面的前端平台仰賴於一個後端與一個資料庫的來源,RESTFul 其實是一個相對舒適的被動解法。反正後端就是開好在那邊,請大家依照文件各取所需就好。但是隨著開發時程拉長、規模變大之後,會發現不同的前端可能會有不同的要求,此時後端可能就需要一個每個來源進行調整或是吵架 (?)

GraphQL 可以優雅地處理這個問題,大概有以下幾點好處:

① No more Over and Less - Overfetching and Underfetching / 前端需要什麼自己決定,可以避免 Response 太肥或不足的問題
② Benefits of a Schema & Type System / 資料類型的定義可以讓達到初步的驗證效果
③ Rich open-source ecosystem and an amazing community / 開源的社群資料很多,也有許多有趣的配套解法可以混搭

Thinking in graphs

前面有提到, GraphQL 將資源視為一個 Graph,這邊的資源可以想成是資料庫中的每一種資料。

怎麼開始、怎麼使用

GraphQL 由幾個部分所組成:

  • TypeDefs
  • Resolvers
  • Queries (Query、Mutation)
  • Schema

=> API contain Schema contain Queries contain TypeDefs and Resolvers

TypeDefs

資料可以定義成下面這樣子,Query 跟 Mutation 作為 Graph 的 Root,其中在包含 Resource 的組合:

1
2
3
4
5
6
7
8
9
10
11
12
type Query {
person: person!
persons: [person]
}
type Mutation {
create(name: String!, age: Int!): String
}

type Person {
name: String!
age: Int!
}

每一種資料都可以設置屬性與類型,可以當作初步的驗證!

Resolvers

可以想成一個 Function,負責解析怎樣的需求該對應怎樣的操作。

Queries (Query、Mutation)

Queries 分為 Query、Mutation 兩種,定義前端的 API 要如何跟後端溝通。

  • Fetching Data with Queries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
query {
Person(last: 1) {
name
},
Persons {
id
name
}
}

# Return:
# {
# "data": {
# "Person": {
# name: "Sarah"
# },
# "Persons": [{
# "id": 1, "name": "Johnny"
# }, {
# "id": 2, "name": "Sarah"
# }]
# }
# }
  • Writing Data with Mutations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mutation {
create(name: "Bob", age: 36) {
id
}
}

# Return:
# {
# "data": {
# "create": {
# "id": 3
# }
# }
# }

Schema

Schema 定義 API 的抽象層,將最上層的 Query Root 往下視為一個 API Graph。

簡單總結如下:

  • TypeDefs ➞ 定義資料 & Query 的型態
  • Resolvers ➞ 定義操作資料的方式
  • Queries (Query、Mutation) ➞ 資料操作的最上層
  • Schema ➞ 定義 API 的抽象層

實踐第一個 GraphQL Server 開始

目前常見的 GraphQL API Server 大概有以下幾種做法與分別來自不同的套件:

  • 第一種寫法:GraphQLObjectType + GraphQLSchema / graphql
  • 第二種寫法:buildSchema + typeDefs + rootValue / graphql
  • 第三種寫法:makeExecutableSchema + typeDefs + resolvers / graphql-tools
  • 第四種寫法:ApolloServer + typeDefs + resolvers / apollo-server
  • 第五種寫法:GraphQLServer + typeDefs + resolvers / graphql-yoga

這邊都是以 Node + Express 為範例,也先將資料來源假設是靜態變數,實務應用的時候可以把 Resolver 改為直接存取資料庫的 Function。

第一種寫法:GraphQLObjectType + GraphQLSchema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

// 1. 先定義資料來源 & 操作資料的 resolvers
let users = [
{ id: 0, name: 'Tom', sex: 0, },
{ id: 1, name: 'Bob', sex: 0, },
{ id: 2, name: 'Alick', sex: 1, },
];

const userResolver = ({id}) => users.filter(u => u.id == id)[0];
const usersResolver = () => users;

// 2. 利用 GraphQLObjectType 定義 data 的 TypeDefs

const userType = new graphql.GraphQLObjectType({
name: 'user',
fields: {
id: { type: graphql.GraphQLInt },
name: { type: graphql.GraphQLString },
sex: { type: graphql.GraphQLInt },
}
})
const usersType = new graphql.GraphQLList(userType)

// 3. 再利用 GraphQLObjectType 定義 Query 的 TypeDefs 且將 resolvers 定義在其中

const queryType = new graphql.GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: userType,
resolve: (_, args) => userResolver(args),
args: {
id: { type: graphql.GraphQLInt }
},
},
users: {
type: usersType,
resolve: () => usersResolver()
}
}
})

// 4. 利用 GraphQLSchema 將 Query 封裝成 Schema

const schema = new graphql.GraphQLSchema({
query: queryType
});

// 5. 利用 graphqlHTTP 將 Schema 封裝成 API

app.use(
'/',
graphqlHTTP({
schema: schema,
graphiql: true
})
);

第二種寫法:buildSchema + typeDefs + rootValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

// 1. 先定義資料來源 & 操作資料的 resolvers
let users = [
{ id: 0, name: 'Tom', sex: 0, },
{ id: 1, name: 'Bob', sex: 0, },
{ id: 2, name: 'Alick', sex: 1, },
];
const userResolver = ({id}) => users.filter(u => u.id == id)[0];
const usersResolver = () => users;

const resolvers = {
Query: {
user: (_, args) => userResolver(args),
users: () => usersResolver()
},
}

// 2. 利用 template string 定義 data 跟 Query 的 TypeDefs

const typeDefs = `
type Query {
user(id: Int!): User
users: [User]
},
type User {
id: Int
name: String
sex: Int
}
`;

// 3. 利用 buildSchema 將 Query 封裝成 Schema

const schema = graphql.buildSchema(typeDefs);

// 4. 利用 graphqlHTTP + rootValue 將 Query 與 resolver 串接且封裝成 API

app.use(
'/',
graphqlHTTP({
schema: schema,
rootValue: {
user: userResolver,
users: usersResolver
},
graphiql: true
})
);

第三種寫法:makeExecutableSchema + typeDefs + resolvers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

// 1. 先定義資料來源 & 操作資料的 resolvers
let users = [
{ id: 0, name: 'Tom', sex: 0, },
{ id: 1, name: 'Bob', sex: 0, },
{ id: 2, name: 'Alick', sex: 1, },
];
const userResolver = ({id}) => users.filter(u => u.id == id)[0];
const usersResolver = () => users;

const resolvers = {
Query: {
user: (_, args) => getCourse(args),
users: () => getCourses()
},
}

// 2. 利用 template string 定義 data 跟 Query 的 TypeDefs

const typeDefs = `
type Query {
user(id: Int!): User
users: [User]
},
type User {
id: Int
name: String
sex: Int
}
`;

// 3. 利用 makeExecutableSchema 將 Query 跟 resolver 封裝成 Schema

const schema = makeExecutableSchema({
typeDefs,
resolvers,
});

// 4. 利用 graphqlHTTP 將 Schema 封裝成 API

app.use(
'/',
graphqlHTTP({
schema: schema,
graphiql: true
})
);

第四種寫法:ApolloServer + typeDefs + resolvers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

// 1. 先定義資料來源 & 操作資料的 resolvers
let users = [
{ id: 0, name: 'Tom', sex: 0, },
{ id: 1, name: 'Bob', sex: 0, },
{ id: 2, name: 'Alick', sex: 1, },
];
const userResolver = ({id}) => users.filter(u => u.id == id)[0];
const usersResolver = () => users;

const resolvers = {
Query: {
course: (_, args) => getCourse(args),
courses: () => getCourses()
},
}
// 2. 利用 template string 定義 data 跟 Query 的 TypeDefs

const typeDefs =gql`
type Query {
user(id: Int!): User
users: [User]
},
type User {
id: Int
name: String
sex: Int
}
`;

// 3. 利用 ApolloServer 將 Query 跟 resolver 封裝成 Schema
const server = new ApolloServer({
typeDefs,
resolvers,
});

// 4. 利用 applyMiddleware 將 server 作為 express 的 Middleware API

const app = express();
server.applyMiddleware({ app });

第五種寫法:GraphQLServer + typeDefs + resolvers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

// 1. 先定義資料來源 & 操作資料的 resolvers
let users = [
{ id: 0, name: 'Tom', sex: 0, },
{ id: 1, name: 'Bob', sex: 0, },
{ id: 2, name: 'Alick', sex: 1, },
];
const userResolver = ({id}) => users.filter(u => u.id == id)[0];
const usersResolver = () => users;

const resolvers = {
Query: {
course: (_, args) => getCourse(args),
courses: () => getCourses()
},
}

// 2. 利用 template string 定義 data 跟 Query 的 TypeDefs

const typeDefs = `
type Query {
user(id: Int!): User
users: [User]
},
type User {
id: Int
name: String
sex: Int
}
`;

// 3. 利用 makeExecutableSchema 將 Query 跟 resolver 封裝成 Schema API

const server = new GraphQLServer({
typeDefs,
resolvers,
})

server.start()

架構與實現

官方將常見的使用案例分為三種,也提出三種可以導入的方式:

① GraphQL server with a connected database
② GraphQL layer that integrates existing systems
③ Hybrid approach with connected database and integration of existing system

GraphQL 也可以分為前端與後端:

① A GraphQL server that serves your API.
=> 套件有: Apollo Client、Relay
② A GraphQL client that connects to your endpoint.
=> 套件有: express-graphql、 apollo-server、 graphql-yoga

除了前後端之外,也可以加一些額外的工具:

  • 前端後端間也可以有一個 Cache Gateway: Apollo Engine
  • 後端與資料庫間也可以用一個 ORM Layer:Prisma、join-monster

Then ?

GraphQL 的第一步可以這樣開始:

  1. 規劃資料庫與資料的關係,Thinking in graphs
  2. 配置後端 GraphQL Server + GraphiQL 測試
  3. 開始串前端!

可以搭配 投影片 閱讀:)

Reference

[1] GraphQL
[2] The Fullstack Tutorial for GraphQL


License


本著作由Chang Wei-Yaun (v123582)製作,
創用CC 姓名標示-相同方式分享 3.0 Unported授權條款釋出。