1 目的
本文介绍一下graphql相关的基础知识,并用go实现一个demo
2 怎么来的
- GraphQL是由Facebook创造的。
当时,Facebook想在移动端实现新闻推送,这不像检索一个故事、作者、故事的内容、评论列表和喜欢该文章的人这么简单,而是每个故事都相互关联、嵌套和递归的。现有的API没有被设计成允许开发人员在移动设备上展示一个丰富、类似新闻推送的体验。它们没有层次性,允许开发人员选择他们所需要的,或有显示异构推送故事列表的能力。因此,2012年Facebook决定自己构建一个新的新闻推送API,这就是GraphQL形成时间。同年 8 月中旬,Facebook 发布了采用新 GraphQL 技术的 iOS5.0 应用程序。它允许开发人员通过利用其数据获取(data-fetching)功能来减少网络的使用。在接下来的一年半时间里,除了新闻推送外,GraphQL API 扩展到大多数的 FacebookiOS 应用程序。在 2015 年,GraphQL 规范首次与 JavaScript 中的引用实现一起发布。
3 是什么
- 一种用于 API 的查询语言。
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
- 通俗解释
和常规sql查询语言不同,它是一种用于前后端数据查询方式的规范。本质就是API查询语言,开发人员可以自定义查询规则,定义自己所需的数据格式,并且在一个请求中获取所有想要的数据。意思就是定义什么返回什么,开发人员对于返回的结果是可预测的。
4 graphql和restful的区别
4.1 示例:对于通过图书ID获取图书详情
- restful
GET /books/1
{
"title": "Black Hole Blues",
"author": {
"firstName": "Janna",
"lastName": "Levin"
}
// ... more fields here
}
- graphql
#1.首先定义数据类型
type Book {
id: ID
title: String
published: Date
price: String
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
books: [Book]
}
#2.创建query
type Query {
book(id: ID!): Book
author(id: ID!): Author
}
#3.http查询
GET /graphql?query={ book(id: "1") { title, author { firstName } } }
{
"title": "Black Hole Blues",
"author": {
"firstName": "Janna",
}
}
对比:
-
相同点:
-
- 都有资源这个概念,而且都能通过ID去获取资源
-
- 都可以通过HTTP GET方式来获取资源
-
- 都可以使用JSON作为响应格式
-
差异点:
-
在RESTful中,你所访问的路径就是该资源的唯一标识(ID);在GraphQL中,该标识与访问方式并不相关(PS:RESTful url是唯一的,GraphQL一般访问地址是一个,查询query不同)
-
在RESTful中,资源的返回结构与返回数量是由服务端决定;在GraphQL,服务端只负责定义哪些资源是可用的,由客户端自己决定需要得到什么资源。
4.2 举个使用场景说明两者差异
为了更好的理解两者的差异,我们用一个场景来说明。比如现在有一个简单的示例场景:在blog应用程序中,应用程序需要显示特定用户的文章的标题。同一屏幕还显示该用户最后3个关注者的名称。REST和GraphQL如何解决这种情况?
- RESTful实现方式:
(1)通过 /user/获取初始用户数据
(2)通过/user//posts 返回用户的所有帖子
(3)请求/user//followers,返回每个用户的关注者列表
- graphql实现方式:
结论: RESTful 请求了3次达到目的,并且接口返回了很多并不需要的数据。GraphQL只请求了一次,并且返回的结果是必需的。
5 有什么用
官网解释:
- 请求你所要的数据不多不少。
- 获取多个资源只用一个请求。
- 描述所有的可能类型系统。
- API 演进无需划分版本。
6 谁在用
7 怎么用
7.1 字段(Fields)
在GraphQL的查询中,请求结构中包含了所预期结果的结构,这个就是字段。并且响应的结构和请求结构基本一致,这是GraphQL的一个特性,这样就可以让请求发起者很清楚的知道自己想要什么。
7.2 参数(Arguments)
在查询数据时,离不开传递参数,在GraphQL的查询中,也是可以传递参数的,语法∶(参数名:参数值)
7.3 别名(Aliases)
如果一次查询多个相同对象,但是值不同,这个时候就需要起别名了,否则json的语法就不能通过了。
7.4 片段(Figments)
片段使你能够组织一组字段,然后在需要它们的地方引入。下面例子展示了如何使用片段解决上述场景:
7.5 Schema和类型
Schema是用于定义数据结构的,比如说,User对象中有哪些属性,对象与对象之间是什么关系等。
scalar Long
schema {#定义查询
query:UserQuery
}
"用户查询" #注释
type UserQuery{#定义查询类型
"根据id查询用户"
getUser(userId:Long):UserVO #指定对象以及参数类型
}
"用户返回对象"
type UserVO{ #定义对象
"用户id"
userId:Long! #!表示属性是非空项
userName:String
age:Int
dept:DeptVO
}
type DeptVO{
deptId:Long
deptName:String
}
7.6 标量类型(Scalar Types)
GraphQL 自带一组默认标量类型:
Int:有符号 32 位整数。 Float:有符号双精度浮点值。 String:UTF‐8 字符序列。 Boolean:true 或者 false。 ID:ID 标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID 类型使用和 String 一样的方式序列化;然而将其定义为 ID 意味着并不需要人类可读型。 大部分的 GraphQL 服务实现中,都有自定义标量类型的方式。例如,我们可以定义一个 Date 类型:
scalar Date
然后就取决于我们的实现中如何定义将其序列化、反序列化和验证。例如,你可以指定 Date 类型应该总是被序列化成整型时间戳,而客户端应该知道去要求任何 date 字段都是这个格式。
7.7 枚举类型(Enumeration Types)
也称作枚举(enum),枚举类型是一种特殊的标量,它限制在一个特殊的可选值集合内。这让你能够:验证这个类型的任何参数是可选值的某一个与类型系统沟通,一个字段总是一个有限值集合的其中一个值。
下面是一个用 GraphQL schema 语言表示的 enum 定义:
7.8 接口(Interfaces)
跟许多类型系统一样,GraphQL 支持接口。一个接口是一个抽象类型,它包含某些字段,而对象类型必须包含这些字段,才能算实现了这个接口。
例如,你可以用一个 Character 接口用以表示《星球大战》三部曲中的任何角色:
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
这意味着任何实现 Character 的类型都要具有这些字段,并有对应参数和返回类型。
例如,这里有一些可能实现了 Character 的类型:
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
}
可见这两个类型都具备 Character 接口的所有字段,但也引入了其他的字段 totalCredits、starships 和 primaryFunction,这都属于特定的类型的角色。
8 demo
8.1 定义请求体
go_graphql/go_schema/fields.go
// 定义请求体
var Fields = graphql.Fields{
"students": &graphql.Field{
// 将返回参数类型加入请求体
Type: StudentsType,
// Type: graphql.NewList(studentsType),
// 定义请求参数
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.Int,
},
"name": &graphql.ArgumentConfig{
Type: graphql.String,
},
},
// 定义接口函数 业务逻辑在接口函数中实现
Resolve: ResolveStudents,
},
}
8.2 定义返回参数
go_graphql/go_schema/student_type.go
//定义返回参数
var StudentsType = graphql.NewObject(
graphql.ObjectConfig{
Name: "students",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
},
"name": &graphql.Field{
Type: graphql.String,
},
"age": &graphql.Field{
Type: graphql.Int,
},
},
},
)
8.3 定义处理函数
go_graphql/go_schema/resolve.go
//业务逻辑函数
var ResolveStudents = func(p graphql.ResolveParams) (interface{}, error) {
// 类型断言
id, _ := p.Args["id"].(int)
name, _ := p.Args["name"].(string)
ret := []map[string]interface{}{}
ret = append(ret, map[string]interface{}{
"id": 1,
"name": "zhangsan",
"age": 18,
}, map[string]interface{}{
"id": 2,
"name": "lisi",
"age": 19,
})
for _, item := range ret {
if item["id"] == id && item["name"] == name {
return item, nil
}
}
return nil, nil
}
8.4 定义schema
go_graphql/go_schema/schema.go
func GetSchema() *graphql.Schema {
rootQuery := graphql.ObjectConfig{Name: "Query", 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)
}
return &schema
}
8.5 开始监听
func main() {
// 用graphql-go/handler中间件处理graphql的http请求
h := handler.New(&handler.Config{
Schema: go_schema.GetSchema(),
Pretty: true,
GraphiQL: true,
})
// 定义路由端口
http.Handle("/test_graphql", h)
http.ListenAndServe(":9090", nil)
}
9 总结
-
API 的查询语言。
-
和RESTful核心差异 资源的描述信息与其获取方式相分离。
-
RESTful服务端决定返回结果,GraphQL客户端决定返回结果。
-
RESTful和GraphQL都是返回json。
其实我个人并不喜欢graphql,它将逻辑放到了前端,后端只是需要定义每个对象和每个属性的值如何取值、设置,我理解业务逻辑应更放到后端,这样才方便做权限控制、监控等整个项目可控。这种不可控的东西放到前端很容易将相同搞挂了,比如恶意攻击者将所以接口都放在一个query里面,后端很容易扛不住。而且后端很难知道哪个接口在什么情况下被调用了,还是那个问题,后端不可控。
10 参考
[1]go + graphql的简单实现
[2]github代码:go_graphql
[3]github代码:graphql-go-example
[4]GraphQL入门基础篇教程
[5]GraphQL 的前世今生
[6]GraphQL和RESTful的区别
[7]github:graphql-go/graphql