graphql学习

553 阅读8分钟

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",
  }
}

对比:

image.png

  • 相同点:

    • 都有资源这个概念,而且都能通过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,返回每个用户的关注者列表

image.png

  • graphql实现方式:

image.png 结论: RESTful 请求了3次达到目的,并且接口返回了很多并不需要的数据。GraphQL只请求了一次,并且返回的结果是必需的。

5 有什么用

官网解释

  • 请求你所要的数据不多不少。
  • 获取多个资源只用一个请求。
  • 描述所有的可能类型系统。
  • API 演进无需划分版本。

6 谁在用

image.png

7 怎么用

7.1 字段(Fields)

在GraphQL的查询中,请求结构中包含了所预期结果的结构,这个就是字段。并且响应的结构和请求结构基本一致,这是GraphQL的一个特性,这样就可以让请求发起者很清楚的知道自己想要什么。

image.png

7.2 参数(Arguments)

在查询数据时,离不开传递参数,在GraphQL的查询中,也是可以传递参数的,语法∶(参数名:参数值)

image.png

7.3 别名(Aliases)

如果一次查询多个相同对象,但是值不同,这个时候就需要起别名了,否则json的语法就不能通过了。

image.png

7.4 片段(Figments)

片段使你能够组织一组字段,然后在需要它们的地方引入。下面例子展示了如何使用片段解决上述场景:

image.png

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 定义:

image.png

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