GraphQl与Golang

REST与GraphQL

如果你还不知道什么是GraphQL,那么,请移驾☞此处

什么实API呢?API的全称是Application Programming Interface,翻译过来就是应用程序编程接口。在网络编程中,API决定了客户端如何从服务器获取数据,或者说API是客户端从服务器获取数据的方式,REST和GraphQL就是这样的方式。

REST和GraphQL本身并不是API,他们是用来修饰API的,也就是说,他们是两种不同的API设计规范,或者说风格。那么他们之间有什么区别呢?

如果你看过爱情公寓的话,有一集中一菲和曾小贤、关谷、子乔打赌。一菲问赌注是什么,子乔说,要是输了就把张伟输给你。一菲很奇怪,我要张伟干什么?这时子乔说了一句很经典的话:和我们赌,不是看你要什么,而是看我们有什么!如果非要形容REST风格的话,我想子乔的这句话应该是最贴切的了。

REST API为客户端提供了一些列端点,每个端点返回特定的数据结构。客户端只能根据自己需要的数据在这些入口点中选择,如果想获得新的结构化数据,就不得不跟服务器商量:嗨,哥们,再提供个API呗。然后等着服务器端不情愿的答复:好了,知道了。

如果说REST是捆绑消费的话,那么GraphQL就是自由消费。假如你的洗发水用完了,你想去超市买瓶洗发水。

你说:我要一瓶洗发水。

REST会说:行,买洗发水送洁厕灵,打八折。

你说:我不要洁厕灵,我就要洗发水。

REST会说:不行,这是活动规则,不要洁厕灵,洗发水就不卖给你。

你:。。。what ?

一脸懵逼的你很想反抗,于是带着洗发水和洁厕灵回家了,不得不接受这惨淡的现实。现在,让我们换成GraphQL再买一次洗发水。

你说:我要一瓶洗发水。

GraphQL会说:行,给你。

就是这么简单,你要什么,GraphQL就能给你什么。当然也不能超出GraphQL所能提供的范围,要是你想买鸽子蛋那么大的钻石,人家上哪儿偷去?

GraphQL只公开单个端点,支持客户端进行声明性数据提取。也就是客户端可以告诉服务器我想要什么数据,然后服务器就把这些数据返回给客户端。

GraphQL被称为是API查询语言,由Facebook实践和开源。服务器公开的单个端点就是一个一个的API,GraphQL提交查询的时候,就像是在查询这些API一样。

如果REST是在以命令的方式要求服务器返回数据,那么GraphQL就是在向服务器描述数据。

使用REST API获取数据时,需要经过四个步骤。

  • 构造并发送HTTP请求
  • 接收和响应服务器响应
  • 在本地存储数据
  • 在UI中显示数据

而使用GraphQL API时只需要两步。

  • 描述数据
  • 显示数据

GraphQL最初用于React语言,但是它可以由任何语言实现。幸运的是,已有Golang的爱好者对此作出了贡献,目前在Github上有三个包可用。如下:

graphql

graphql-go

magellan

其中第一个包显得有些笨拙,它大量使用了空接口来实现。第二个包和第三个包非常类似,使用了静态代码分析来实现。它们之间的另一个重大区别是,第二个包和第三个包支持SDL(Schema Definition Language:模式定义语言),而第一个包不支持。更多关于SDL的介绍会在后面展开。

我主要使用第二个包,第一个包会用一个例子做一个入门介绍。

使用GraphQL可以带来哪些好处呢?

  1. 减少需要通过网络传输的数据量,提高数据加载效率,因为买洗发水不会送你洁厕灵。

  2. 前端差异透明化。如果我和你一起去超市买东西,我要买洗发水和沐浴露,而你要买沐浴露和洗面奶,而最终的结果可能是REST将洗发水、沐浴露和洗面奶捆绑销售,我买了不需要的洗面奶,你买了不需要的洗发水。但GraphQL会将这三样东西分开卖,这样,我和你可以根据自己的需要选择商品,这样客户端的差异在服务端就透明化了。

  3. 灵活、快速开发。当沐浴露和洗发水的组合卖的热火朝天的时候,突然有一天你要买沐浴露和洗面奶,如果将洗面奶加到洗发水和沐浴露的组合中,又可能失去之前的客户,于是REST就不得不再推出一款洗面奶和沐浴露的组合。但是如果是GraphQL,完全不用对API做出任何改动,由客户端改变选择就行了。

总结起来就是灵活、高效。

为什么是GraphQL

GraphQL的出现号称是为了解决REST存在的问题,知道REST的问题,就相当于是知道了GraphQL的优势。

问题1:过度和不足

过度意味着下载了多于的数据,就好比买洗发水送的洁厕灵。不足意味着靠一个API无法得到所需的所有数据,由此就会引发n+1问题,也就是说客户端必须发起额外的请求才能获得所需数据。这就好比如果我需要的是洗发水和洗面奶,那么我就不得不买两个套餐。

问题2:限制前段迭代速度

如果后端定下来了,那么前端获取数据的方式也就确定了。如果这时前端需要改动,那么很有可能连同后端也需要作出适当的修改。而在GraphQL中,前端的改动几乎不会要求后端作出任何改动。

GraphQL还带来了另外两个优势。

优势1:后端分析

超市每个月要做的事就是分析商品的销售形式,卖的火的要增加库存,并把没人买的商品及时下架。GraphQL允许你对请求数据做细粒度的分析,了解哪个客户端对那些数据感兴趣,并弃用任何客户端都不再请求的字段。如果洁厕灵和洗发水绑定在了一起,而洁厕林又没人买的话,那么它只能长期占用库存。

此外,还可以对请求进行性能监视。GraphQL通过解析器函数返回客户端请求的数据,监测这些解析器的性能可以帮助你找到系统新能的瓶颈。

优势2:前后端独立开发

这一点依赖于GraphQL的Schema和类型系统,Schema和类型系统是GraphQL的发明。Schema是用SDL编写的,它是服务器和客户端之间的契约。Schema定义了客户端访问数据的方式,就像是一份商品目录清单,告诉你在这个超市中你可以买到哪些东西。类型系统则定义了API的功能,具体来说是定义了API应该返回什么样的数据。比如说,超市应该卖洗发水,可以是海飞丝,也可以是飘柔,还可以是蒂花之秀,但不能是舒肤佳,因为类型系统规定了必须是洗发水。

一旦Schema定义好了,前端和后台就可以各干各的了,因为后台知道自己该提供什么数据,前端也知道自己可以获取哪些数据。就好比一旦商品目录清单确定了,采购和导购员就可以各自干活了。

Hello World

准备工作:下载github.com/graph-gophers/graphql-go包。

go get github.com/graph-gophers/graphql-go

下面的代码可能会让你一头雾水,但是不要问为什么,因为必须这么写,所有你心中的疑惑,我都会在后面解开。

首先我们需要写一段Schema代码,用SDL语言写。

var s = `
        schema {
            query: Query
        }
        type Query {
            hello: String!
        }
`

因为是在Golang中,所以这段SDL语言写的代码只能以字符串的形式存在。schema定义的就是GraphQL的Schema,它是一份清单,定义客户端可以进行的操作,这里是query,表示客户端可以进行查询操作。type对应的就是类型系统,它定义了Query对象,这个对象有一个String类型的字段,叫做helloString后面的感叹号表示该字段为非空字段,也就是说当客户端查询这个字段时,服务器一定会返回一个值。

Schema只是一份契约,至于契约的执行,也就是如何提供数据以及提供什么样的数据还需要Go的支持。

type query struct{}
func (_ *query) Hello() string {
    return "hello world"
}

这里的query结构体就是解析器,它的方法Hello就是解析器函数。

下一步缔结契约,既然双方都已准备好,签了字契约才算生效。

var schema = graphql.MustParseSchema(s, &query{})

还差最后一步,为GraphQL API开通Http服务,直接用Golang提供的Http服务就行了。不过在此之前我们需要导入github.com/graph-gophers/graphql-go/relay包,用它提供的功能将Schema转换成Golang处理器。

func main() {
    h := relay.Handler{schema}
    http.Handle("/hello", &h)
    http.ListenAndServe(":8080", nil)
}

服务端的代码就编写完了,接下来是怎么玩的问题了。先将程序运行起来,现在是万事具备,只欠东风。

如果有安装curl的话,可以打开powershell,输入如下命令。

curl -X POST -d '{\"query\":\"{hello}\"}' localhost:8080/hello

如果一切正常,将会得到下面的输出。

{"data":{"hello":"hello world"}}

这是一段json字符串,表明查询结果是hello world

为什么要用这种方式?因为relay提供的处理器是通过解析Body中的json串来获得客户端的查询请求的,使用curl是最简单的测试方式了,后面我会用一种更加好看的方式。

将curl POST的内容展开就是下面的内容。

{
    "query": "{hello}"
}

其实真正扮演GraphQL查询角色的是{hello},这样写也是语法使然。而"query"的存在只是为了处理器能正确的提取出后面的{hello}。当你买洗发水的时候,你需要跟服务员说:我要一瓶洗发水,飘柔的。你说的这些话就如同这里的{hello},它告诉服务器我要查询hello字段。

通过这个例子,我们已经了解到了GraphQL两个重要的内容:Schema和查询。更多的语法会在后面介绍,我们的主要工作也在于编写这两部分的内容。

附:完整代码☟

package main

import (
	"net/http"
	graphql "github.com/graph-gophers/graphql-go"
    "github.com/graph-gophers/graphql-go/relay"
)

type query struct{}

func (_ *query) Hello() string {
	return "hello world"
}

var s = `
		schema {
			query: Query
		}
		type Query {
			hello: String!
		}
	`
var schema = graphql.MustParseSchema(s, &query{})

func main() {
	h := relay.Handler{schema}
	http.Handle("/query", &h)
	http.ListenAndServe(":8080", nil)
}

作为对比,我们来看看使用github.com/graphql-go/graphql包如何写出Hello World。

首先还是需要下载这个包。

go get github.com/graphql-go/graphql

套路还是一样的,需要编写Schema。不同的是这次草拟契约和签订契约不再是两件事,而是变成了一件事。这就意味着,我们不再使用SDL语法以字符串的形式编写Schema了,而是要用Golang语法来编写。

首先需要写类型系统,与前边对应的是type Query{hello:String!}这部分代码。

var queryType = graphql.NewObject(graphql.ObjectConfig {
    Name: "Query",
    Fields: graphql.Fields{
        "hello": &graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (interface{}, err) {
                return "hello world", nil
            },
        },
    },
})

这么长一段就说明了一件事:有一个叫做Query的GraphQL对象,它有一个叫hello的字段,类型是String,而Resolve函数定义了该字段的解析器函数,也就是说当客户端查询hello字段时,就会调用这个函数。

然后是定义Schema,对应前面schema{query:Query}这段代码。

var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{Query: queryType})

最后一步是把Schema挂到Http服务上,不幸的是,这个包没有提供直接将Schema转为Handler的功能。然而万幸的是,有另一个包提供了这一功能。不过俗话说的好,自己动手,丰衣足食。我想说的是,我们不妨先自己造个轮子,然后在用用别人的轮子。

造轮子

graphql包提供了Do函数来执行查询,需要Schema和查询字符串作为参数,我们只需要在处理器中调用这个函数就可以完成GraphQL查询了。

func hello(w http.ResponseWriter, r *http.Request) {
    result := graphql.Do(graphql.Params{
        Schema: schema,
        RequestString: r.URL.Query().Get("query"),
    })
    if len(result.Errors) > 0 {
        fmt.Fprintf(w, "Wrong result, unexpected errors: %v", result.Errors)
        return
    }
    json.NewEncoder(w).Encode(result)
}

最后main函数只需要像普通的Http程序那样开启Http父服务就行了。

func main() {
    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8080", nil)
}

迫不及待想看看结果了吗?在curl中输入curl localhost:8080/hello?'query=\{hello\}',回车之后就能看到服务器发回的数据了,和第一个例子一模一样。此外,也可以在浏览器中输入localhost:8080/hello?query={hello},也能得到相同的结果。

完整代码:

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"github.com/graphql-go/graphql"
)

var queryType = graphql.NewObject(
	graphql.ObjectConfig{
		Name: "Query",
		Fields: graphql.Fields{
			"hello": &graphql.Field{
				Type: graphql.String,
				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
					return "hello world", nil
				},
			},
		},
	},
)

var schema, _ = graphql.NewSchema(graphql.SchemaConfig{Query: queryType})

func hello(w http.ResponseWriter, r *http.Request) {
    result := graphql.Do(graphql.Params{
        Schema: schema,
        RequestString: r.URL.Query().Get("query"),
    })
    if len(result.Errors) > 0 {
        fmt.Fprintf(w, "Wrong result, unexpected errors: %v", result.Errors)
        return
    }
    json.NewEncoder(w).Encode(result)
}

func main() {
	http.HandleFunc("/hello", hello)
	http.ListenAndServe(":8080", nil)
}

别人家的轮子

不管怎么说自己造的轮子还是有点简陋,好在github.com/graphql-go/handler包提供了GraphQL转Http处理器的支持。让我们下载过来试一试。

go get github.com/graphql-go/handler

现在我们不用自己写处理器函数了,mian函数需要做一点修改。

func main() {
    h := handler.New(&handler.Config{
        Schema: &Schema,
        pretty: true,
        GraphiQL: true,
    })
    http.Handle("/hello", h)
    http.ListenAndServe(":8080", nil)
}

在浏览器中输入localhost:8080/hello,哇喔~ 有没有很酷。
在这里插入图片描述

左边区域用于输入查询,点击长得很像播放的那么按钮,右边就会显示查询结果。
在这里插入图片描述

这样就方便和直观多了,不得不承认,别人造的轮子就是比自己造的好,果然都是别人的的轮子。

完整代码:

package main

import (
	"net/http"
	"github.com/graphql-go/graphql"
	"github.com/graphql-go/handler"
)

var queryType = graphql.NewObject(graphql.ObjectConfig{
	Name: "Query",
	Fields: graphql.Fields{
		"hello": &graphql.Field{
			Type: graphql.String,
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
				return "hello world", nil
			},
		},
	},
})

var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{Query: queryType})

func main() {
	h := handler.New(&handler.Config{
		Schema:   &Schema,
		Pretty:   true,
		GraphiQL: true,
	})
	http.Handle("/hello", h)
	http.ListenAndServe(":8080", nil)
}

小结

对比这两个例子,graphql包大量使用了结构体嵌套来模仿SDL,虽然结构清晰,不需要额外编写SDL代码,但是代码量却是有增无减,一旦功能复杂起来,代码结构会不忍直视。唯一的优势是有配套的处理器转换包,不过我们也可以为graphql-go包写一套一模一样的处理器转换包。反观graphql-go包,代码就简洁多了,而且SDL代码可以让人一眼就看出程序的功能。

综上所述,换而言之,后面的学习中,我们只会用graphql-go这个包了。就决定是你了,去吧,皮卡丘~

劳动改造

为了在使用github.com/graph-gophers/graphql-go包时也有漂亮的轮子可以用,我们需要fork一下github.com/graphql-go/handler包,并做一点改造。

需要改动的地方不是很多,并且你可以根据自己的习惯,做出个性化的修改和定制。我属于有点强迫症的人,所以做了一些定制,怎么改其实无所谓,只要你自己喜欢而且能用就行。

为什么要在这里强调这一点呢?因为后面的学习中,我就全部改用自己改造的包来生成处理器了,这并不是最重要的部分,只是一种可以更加清晰和直观的看到代码效果的方式。

如果你跳过了这一章,或者因为它不重要而放弃。那么在后面你可能会看到一些奇怪的代码,在你的机器上编译器会告诉你找不到某些函数,或者在你那里看不到和我一样的效果。对于初学者,这是很糟糕的,我不希望因为这些本不重要的事而阻碍了你学习的进度和热情。我也是从菜鸟走过来的,虽然现在任然很菜,我要说的是我能体会初学者的心情,因此本章的内容务必认真完成,然后再往下走。

首先在你的GOPATH下新建一个目录GraphQL-Handler,然后将handler文件夹拷贝进去。这个过程完全可以自定义。我的目录是这样的,如果你建的目录和我的不一样,记得导入handler包的时候换成你自己的目录就行了。

github.com/graphql-go/handler包的内容并不多,我们只需要改其中两个脚本就可以使用了。
在这里插入图片描述

至于测试文件,它们并不会影响使用,甚至你可以将它们删除。但是写测试文件是一个好习惯,并且是必要的,这里暂时不修改它们只是为了不增加不必要的复杂度,因为我们并不打算发布它。

首先,在handler.go文件中,我们需要添加一个代表查询的结构体和两个执行查询的方法。
在这里插入图片描述

然后再增加一个生成处理器的函数。
在这里插入图片描述

最后需要修改handler.go中的ContextHandler函数。
在这里插入图片描述

handler.go就修改完了,再次声明,这只是一种符合我习惯的修改方式,不是必须这么做。

graphigl.go文件的修改就简单多了,只需要修改renderGraphiQL函数就可以了。
在这里插入图片描述

修改完以后可以用前面的例子测试一下,在[Hello World](#Hello World)的第一个例子中,首先导入我们修改过的handler包。

import “GraphQL-Handler/handler"

然后将main函数修改如下。

func main() {
    h := handler.HttpHandler(schema, true, true)
	http.Handle("/hello", h)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

在浏览器中输入localhost:8080/hello回车,看到如下图的界面就OK了。

在这里插入图片描述

对象与字段

编写Schema非常像是在定义结构体,还是以[Hello World](#Hello World)中的例子来说明。

schema {
    query: Query
}
type Query {
    hello: String!
}

这是最简单的Schema了,schema关键字用来定义一个Schema;query是它的字段;Queryquery字段的类型。但是这个类型并不是GraphQL的类型,而是我们自定义的对象类型,紧接着我们就用type关键字解释了Query类型。

在GraphQL中用type ObjectName{...}来定义对象,对象也是类型。用Field:Type来为对象定义字段,这里的Type既可以是GraphQL的标量类型,也可以是自定义的对象类型,甚至枚举类型,这些类型都会在后面介绍。

schema的定义十分刻板,它只能有三个字段:querymutationsubscription。这三个字段分别代表查询、修改和订阅。对比REST,query就相当于GET请求,mutation就相当于POSTDELETE请求。因为现在只涉及查询,mutationsubscription会在后面专门介绍。

query必须是一个对象类型。必须是Query吗?当然不是,可以是任意名字,只要是一个对象类型,这样写只是一种习惯,并不是语法约束。

关键字type用来定义对象,一个对象可以拥有若干字段。这里Query对象拥有hello字段,类型是String类型。GraphQL中的标量类型还包括IntFloatBoolean。类型后面加上!就表示该字段为非空字段,换句话说,当你查询这类字段时,一定会给你返回一个值。就好比食盐是超市必须卖的商品,当你去超市买食盐的时候,他绝不会跟你说没有。

那么Schema和查询有什么关系呢?在上一个例子中,我们测试程序时是输入下面的内容进行查询的。

{
    hello
}

其实在大括号中输入的内容就是Query对象的字段。那么这和schema有什么关系呢?其实上面的查询是简写版,我们省略了query关键字,完整的查询应该像下面这样。

query {
    hello
}

QraphQL允许我们在查询时省略query关键字,以大括号开头的默认就是查询。现在插叙和Schema之间的关系就非常明朗了,我们查询query字段的hello字段,其实就是Query对象的hello字段。

编写Schema的工作也就是在定义对象和字段,知道了如何定义字段和对象,我们就可以来实现一个复杂点的例子了,并以此填上一些坑。

####开工

查询一定是以数据为基础的,现在假设我们有一个film结构体,定义如下。

type film struct {
    Name    string  //电影名
    Country string  //国家
    Year    int32   //年份
    Runtime int32   //时长(分钟)
    Color   bool    //是否为彩色
    Score   float64 //评分
}

为了客户端能够查询这个结构体的数据,我们需要编写一个Schema。按照前面例子的经验,我们可以这样编写。

var s = `
		schema {
    		query: Query
		}
		type Query {
    		name   : String!
    		country: String!
    		year   : Int!
    		runtime: Int!
    		color  : Boolean
    		score  : Float!
		}
`

这样写是可以的,但不是最好的。这样会带来两个问题:一是结构混乱。因为写在Query对象中的字段明显不是属于它的,而是属于Film对象的。二是使Query对象变得非常庞大。如果有几十上百个结构体数据,而所有字段都在写在Query对象中,可想而知,Query对象将变得多么复杂和庞大。无论是哪一点,无疑都会增加代码维护的难度。因此,我们再定义一个Film对象,分担Query对象的压力。

var s = `
		schema {
            query: Query
        }
        type Query {
            film: Film
        }
        type Film {
            name   : String!
            country: String!
            year   : Int!
            runtime: Int!
            color  : Boolean
            score  : Float!
        }
`

这样看起来的清晰明了多了。不过请特别注意一点,只有color字段的类型Boolean后面没有加!,加不加!并无强制规定,只是先记住这里的Boolean后面没有!,后面用得上。

第一步已经完成了,下面就是要得到Golang支持,也就是为每个字段编写解析器函数。再次回顾一下,Schema的每个对象对应着一个解析器,也就是Golang的结构体;每个字段对应一个解析器函数,也就是Golang结构体的一个方法。需要说明的是,解析器函数的名字必须和Schema中的字段名字一样,一般习惯是字段名全小写,解析器函数名就是首字母大写的字段名。一定一定,不要随便写解析器函数的名字。

这一次,我们需要从下至上编写解析器函数。为什么?因为Query对象的film字段并不是一个标量类型,可以直接解析,它是Film对象类型,意味着它也对应着一个解析器。反观Film对象的字段都是标量类型,直接对应解析器函数。

type filmResolver struct {
    f *film
}

func (r *filmResolver) Name() string {
	return r.f.Name
}

func (r *filmResolver) Country() string {
	return r.f.Country
}

func (r *filmResolver) Year() int32 {
	return r.f.Year
}

func (r *filmResolver) Runtime() int32 {
	return r.f.Runtime
}

func (r *filmResolver) Color() *bool {
	return &r.f.Color
}

func (r *filmResolver) Score() float64 {
	return r.f.Score
}

如果你观察力敏锐的话,应该注意到Color函数返回的是*bool类型,而其他方法都是返回的值类型。如果注意到了这一点,先记住。

Query对象的film字段是Film对象类型,而这个对象有自己的解析器,因此,film字段的解析器函数只需要返回Film对象的解析器就可以了。问题在于,filmResolver需要一个film类型的字段,这就是数据。查询是基于数据的,因此在编写Query对象的解析器之前,还需要伪造一份数据。

var Hidden_Man = &film{
	Name:    "邪不压正",
	Country: "China",
	Year:    2018,
	Runtime: 137,
	Color:   true,
	Score:   7.1,
}

目前数据是我们自己伪造的,实际应用中应该是查询数据库得到的,有了数据,就可可以编写Query对象的解析器了。

type Resolver struct{}

func (r *Resolver) Film() *filmResolver {
    return &filmResolver{Hidden_Man}
}

双方都已准备就绪,是时候缔结契约了。

var schema = graphql.MustParseSchema(s, &Resolver{})

最后是main函数。这次我们用自己改造过的轮子来生成处理器。所以要记得先导入上一章改造过的handler包。

import "GraphQL-Handler/handler"

func main() {
	h := handler.HttpHandler(schema)
	http.Handle("/film", h)
	http.ListenAndServe(":8080", nil)
}

####安排~

运行程序,在浏览器中输入localhost:8080/film回车,让我们来试试查询Hidden_Man的各个字段。需要注意的是,现在film不是一个可直接查询的字段,它本身也是一个解析器,只有那些标量类型的字段才能直接查询,所以现在查询应该这样写。

{
    film {
        name
        country
        Year
        Runtime
        score
        Color
    }
}

查询结果应该如下图。
在这里插入图片描述

在结束本章之前,还有几个问题需要澄清一下,前面有两个地方让大家先记住。一是在Schema中定义Film对象时只有Color字段的类型Boolean后面没有加!;二是Color字段的解析器函数返回的是*bool类型。

现在我要提出这样四个问题。

  1. Film对象的其他字段的类型后可不可以也不加! ?
  2. Color字段的解析器函数可不可以返回bool类型的值?
  3. Year字段和Runtime字段的解析器函数可不可以返回int类型?
  4. Score字段的解析器函数可不可以返回float32类型?

以上四个问题的答案都是否定的,不信可以亲自试一试。

这四个问题揭示了三个实现上的规则,是只有用Golang时才会有的规则,不是GraphQL本身的规则。

  1. 如果Schema中对象的某个字段可以为空,也就是其类型后没有!的话,那么它的解析器函数必须返回指针类型,而不能是值类型。
  2. GraphQL中的Int类型必须对应Golang中的int32类型。
  3. GraphQL中的Float类型必须对应Golang中的float64类型。

收工,本章完~

附:完整代码☟

package main

import (
	"GraphQL-Handler/handler"
	"net/http"
	graphql "github.com/graph-gophers/graphql-go"
)
// Schema
var s = `
		schema {
    		query: Query
		}
		type Query {
    		name   : String!
    		country: String!
    		year   : Int!
    		runtime: Int!
    		color  : Boolean
    		score  : Float!
		}
`

type film struct {
    Name    string  //电影名
    Country string  //国家
    Year    int32   //年份
    Runtime int32   //时长(分钟)
    Color   bool    //是否为彩色
    Score   float64 //评分
}

var Hidden_Man = &film{
	Name:    "邪不压正",
	Country: "China",
	Year:    2018,
	Runtime: 137,
	Color:   true,
	Score:   7.1,
}
//Query解析器
type Resolver struct{}

func (r *Resolver) Film() *filmResolver {
	return &filmResolver{Hidden_Man}
}

type filmResolver struct {
	f *film
}
//Film解析器
func (r *filmResolver) Name() string {
	return r.f.Name
}

func (r *filmResolver) Country() string {
	return r.f.Country
}

func (r *filmResolver) Year() *int32 {
	return &r.f.Year
}

func (r *filmResolver) Runtime() *int32 {
	return &r.f.Runtime
}

func (r *filmResolver) Color() *bool {
	return &r.f.Color
}

func (r *filmResolver) Score() float64 {
	return r.f.Score
}

var schema = graphql.MustParseSchema(s, &Resolver{})

func main() {
	h := handler.HttpHandler(schema)
	http.Handle("/", h)
	http.ListenAndServe(":8080", nil)
}

更多类型

列表类型

上一个例子中,我们定义了一个描述电影的结构体,这个结构体包含的信息显然不是那么完整。比如说,就没有主演信息。现在让我们把主演也加上,顺便学习一下SDL的列表类型。

和许多语言一样,SDL的列表类型也用方括号来定义,只不过类型类型写在方括号中间。比如[String]表示字符串列表,[Int]表示整数列表,列表的类型不仅可以是标量类型,也可以是自定义的对象类型。

列表也是一种类型,所以列表类型也可以在后面加上!表示非空。比如[String]!,表示列表不能为空,但列表中的值可以为空,这时它的解析器函数应该返回一个切片指针。注意区别[String!],它表示的含义刚好相反,列表可以为空,但列表中的值不能为空,它的解析器函数返回一个切片,切片中的元素应该是字符串指针类型。

在示例中,我大量使用了!来进行非空约束,并不是必须这么做。一部分原因是这些信息不应该为空,另一个原因是为了编写解析器函数的方便,大家只要记住不加!的类型必须对应Golang的指针类型就行了。

开工吧~

首先给Schema的Film对象添加一个stars字段,类型为[String!]!,表示列表和列表中的值都是非空的。

var s = `
		schema {
            query: Query
        }
        type Query {
            film: Film
        }
        type Film {
            name   : String!
            stars  : [String!]!	#主演
            country: String!
            year   : Int!
            runtime: Int!
            color  : Boolean
            score  : Float!
        }
`

SDL语言的注释以#开头,一门语言要是不支持注释简直是过分,SDL的注释看起来还不错。

然后需要在结film构体中也增加一个字段,然后更新一下数据。

type film struct {
    Name    string   //电影名
    Stars   []string //主演
    Country string   //国家
    Year    int32    //年份
    Runtime int32    //时长(分钟)
    Color   bool     //是否为彩色
    Score   float64  //评分
}

var Hidden_Man = &film{
	Name:    "邪不压正",
	Stars:   []string{"姜文", "彭于晏", "廖凡", "周韵", "许晴"},
	Country: "China",
	Year:    2018,
	Runtime: 137,
	Color:   true,
	Score:   7.1,
}

最后给stars字段添加一个解析器函数就OK了。

func (r *filmResolver) Stars() []string {
	return r.f.Stars
}

在浏览器中输入localhost:8080/film,然后查询一下这部电影有哪些主演吧。
在这里插入图片描述

枚举类型

一部电影还有它的类型,比如喜剧、动作、武侠等等。电影类型是一些固定类型中的一个,适合用枚举类型来表示。SDL通过enum关键字来定义枚举类型,然而令人有些沮丧的是,Golang并不支持枚举类型。

题外话:虽然Golang并没有enum关键字直接支持枚举类型,但是我们可以通过constiota来实现枚举的功能。比如实现一个星期的枚举类型:

const(
Sunday = iota //0
Monday        //1
Tuesday       //2
Wedenesday    //3
Thursday      //4
Friday        //5
Saturday      //6
)

枚举类型其实就是一系列常量,通过上面这种方式定义的常量任然是int类型,而不是枚举类型。

那么在这个包中,是如何处理枚举类型的呢?

直接怼字符串。没错,就是这么粗暴。首先在Schema中定义一个Style枚举类型,并在Film对象中添加style的字段,类型就是Style类型。

var s = `
		schema {
            query: Query
        }
        type Query {
            film: Film
        }
        type Film {
            name   : String!
            stars  : [String!]!
            country: String!
            style  : Style!	#类型
            year   : Int!
            runtime: Int!
            color  : Boolean
            score  : Float!
        }
        enum Style {	#枚举类型
        	DRAMA		#剧情
			COMEDY		#喜剧
			ACTION		#动作
			WAR			#战争
			HORROR		#恐怖
			DOCUMENTARY	#记录
        }
`

file结构体和数据也要做出更新。

type film struct {
    Name    string   //电影名
    Stars   []string //主演
    Country string   //国家
    Style   string   //类型
    Year    int32    //年份
    Runtime int32    //时长(分钟)
    Color   bool     //是否为彩色
    Score   float64  //评分
}

var Hidden_Man = &film{
	Name:    "邪不压正",
	Stars:   []string{"姜文", "彭于晏", "廖凡", "周韵", "许晴"},
	Country: "China",
    Style:   "COMEDY",
	Year:    2018,
	Runtime: 137,
	Color:   true,
	Score:   7.1,
}

这里并没有任何机制来检测Style字段值的合法性,因此一定要自觉。如果你赋给Style字段的值是一个其他单词,那么客户端就会得到一个奇怪的结果。如果你在这里皮了一下,那么查询时,服务器也会皮一下。

style字段的解析器函数如下。

func (r *filmResolver) Style() string {
	return r.f.Style
}

运行程序,查询一下电影类型吧。
在这里插入图片描述

其实这并不是使用枚举类型的正确姿势,正确姿势是用在参数中,不过这是后话了。现在要考虑的问题是,一部电影可能不止一个类型,比如《邪不压正》这部电影就有三个类型标签:剧情、喜剧、动作。其实这也很容易,用列表类型的搞定了。

先把Film对象的style字段变成列表类型。

type Film {
	... ...
	style : [Style!]!	#类型
	... ...
}

film结构体的Style字段需要变成字符串切片类型。

type film struct {
    ... ...
	Style : []string
    ... ...
}

更新数据。

var Hidden_Man = &film {
    ... ...
    Style : []string{"DRAMA", "COMEDY", "ACTION"}
    ... ...
}

更新解析器函数。

func (r *filmResolver) Style() []string {
	return r.f.Style
}

运行程序,查看新的查询结果。
在这里插入图片描述

ID类型

前面提到SDL的基础类型,也称标量类型,有IntFloatBooleanString类型,除了这些常见的类型之外,其实还有一个ID类型。没错,就是ID类型。

ID类型表示一个唯一标识符,和数据库或者哈希表的键是非常类似的。那么它究竟长什么样子呢?ID类型采用和String类型一样的方式序列化,这就意味着ID类型不仅仅是一串数字,也可以包含字母或者其他人类看不懂的字符。简单的说,你可以就把它当做字符串来对待。

那么电影有没有一个唯一的标识符呢?

当然是有的,IMDb是一个全球电影资源数据库,它里面的每部电影都有一个编号,并且是唯一的,那么怎么获得这个编号呢?

打开豆瓣电影,没错,就是豆瓣电影官网。找到邪不压正这部电影,点击打开。在介绍中就有这部电影的IMDb编号,如下图。
在这里插入图片描述

其实,豆瓣电影也有它自己的电影编号,注意看地址栏中有一串数字,这就是电影在豆瓣的编号。
在这里插入图片描述

这两个编号都可以用,我选择IMDb编号的原因是它还有字母,可以更好的说明ID类型不一定都是数字。

首先在Schema的Film对象中添加一个id字段。

var s = `
		schema {
            query: Query
        }
        type Query {
            film: Film
        }
        type Film {
        	id     : ID!	#电影ID
            name   : String!
            stars  : [String!]!
            country: String!
            style  : [Style!]!
            year   : Int!
            runtime: Int!
            color  : Boolean
            score  : Float!
        }
        enum Style {	#枚举类型
        	DRAMA		#剧情
			COMEDY		#喜剧
			ACTION		#动作
			WAR			#战争
			HORROR		#恐怖
			DOCUMENTARY	#记录
        }
`

SDL的ID类型在Golang中对应着graphql.ID类型,它并不是什么新类型,其实它就是string类型的别名,只不过在ID类型上实现了序列化和反序列化方法。

film结构体中添加ID字段,并更新数据。

type film struct {
    ID      graphql.ID //编号
    Name    string     //电影名
    Stars   []string   //主演
    Country string     //国家
    Style   []string   //类型
    Year    int32      //年份
    Runtime int32      //时长(分钟)
    Color   bool       //是否为彩色
    Score   float64    //评分
}

var Hidden_Man = &film{
    ID:      "tt8434380",
	Name:    "邪不压正",
	Stars:   []string{"姜文", "彭于晏", "廖凡", "周韵", "许晴"},
	Country: "China",
    Style:   []string{"DRAMA", "COMEDY", "ACTION"},
	Year:    2018,
	Runtime: 137,
	Color:   true,
	Score:   7.1,
}

添加id字段的解析器函数。

func (r *filmResolver) ID() graphql.ID {
	return r.f.ID
}

试一试查询电影的编号。
在这里插入图片描述

当然,这也不是使用ID类型的正确姿势,ID类型也多用于查询时的参数,这也是后话了。

接口类型

一部电影的信息中当然还应该包括最重要的导演。要添加导演不过是一个字段的事儿,当然不能这么轻松。如果我还想获得关于导演的更多信息呢?比如生日、性别、出生地等等,于是我们不得不将导演定义为一个对象类型。

前面我们添加了主演信息,我们也可以让演员信息更丰富一些,而不是只有他们的名字,因此演员也可以定义为一个对象类型。可是转念一想,导演和演员都是人,有很多字段都是相同的,于是接口类型就应运而生了。不错,GraphQL还支持接口!

GraphQL定义接口和定义对象并无多大区别,唯一的区别就是定义接口的关键字是interface,而不是type,接口中定义了所有对象共有的字段。

继承接口时需要通过关键字implements声明继承的是哪个接口,同时对象中还必须包含接口中的所有字段。

不得不说,这一章的工作量是有史以来最大的。让我们先在Schema中添加一个Person类型的接口,以及两个继承至该接口的类型:ActorDirector。就不给出所有代码了,直接在原来的字符串后面追加一下内容就可以了。

		interface Person {
			id  	: ID!
			name	: String!
			birthday: String!
			sex 	: String!
		}
		type Actor implements Person {
			id  	: ID!
			name	: String!
			birthday: String!
			sex 	: String!
			website	: String!
		}
		type Director implements Person {
			id  	: ID!
			name	: String!
			birthday: String!
			sex 	: String!
			company	: String!
		}

咦?演员和导演也有id吗?

是的,IMDb和豆瓣也给每个演员编了号,在豆瓣电影详情页面点击演员名字就能打开链接。还是相同的位置,使用哪一个随你选择,为了保持一致性,我就选用了IMDb的编号。
在这里插入图片描述

需要注意的是,虽然Person接口中已经列出了公共字段,但是,Actor类型Director类型依然要把公共字段再定义一遍,然后再定义它们特有的字段。由于演员和导演的信息差别实在是太少了,所以这里的定义总觉得很牵强,如果你有好的想法,可以用你自己的定义。

在Golang中也要对应的定义actordirector结构体,并实现它们的解析器。首先是actor类型,如果你对前面的例子很熟悉的话,那么这里就不会有太大问题,甚至可以自己写出来。

type actor struct {
	ID       graphql.ID //imdb编号
	Name     string     //姓名
	Birthday string     //生日
	Sex      string     //性别
	Website  string     //主页
}

type actorResolver struct { // actor类型的解析器
	a *actor
}

func (r *actorResolver) ID() graphql.ID {
	return r.a.ID
}

func (r *actorResolver) Name() string {
	return r.a.Name
}

func (r *actorResolver) Birthday() string {
	return r.a.Birthday
}

func (r *actorResolver) Sex() string {
	return r.a.Sex
}

func (r *actorResolver) Website() string {
	return r.a.Website
}

再来director类型,和actor类型并无区别。

type director struct {
	ID       graphql.ID //imdb编号
	Name     string     //姓名
	Birthday string     //生日
	Sex      string     //性别
	Company  string     //公司
}

type directorResolver struct { // director类型的解析器
	d *director
}

func (r *directorResolver) ID() graphql.ID {
	return r.d.ID
}

func (r *directorResolver) Name() string {
	return r.d.Name
}

func (r *directorResolver) Birthday() string {
	return r.d.Birthday
}

func (r *directorResolver) Sex() string {
	return r.d.Sex
}

func (r *directorResolver) Company() string {
	return r.d.Company
}

Schema中的一个接口类型也对应着Golang中的一个接口,Schema中接口的字段则对应Golang接口中的方法。接口同样需要解析器和解析器函数,只不过接口的解析器函数比较特殊。它并不是解析接口的字段,因为那没有意义,接口中的公共字段应该由实现该接口的具体类型负责解析。那么接口类型的解析器函数是干嘛的呢?

接口类型的解析器函数需要将接口类型的解析器转换成实际类型的解析器。也就是做解析器类型转换,同时接口类型解析器函数的个数和实现该接口的实际类型的个数一样,也就是说,接口类型和每个实现该接口的实际类型之间都要可以自由转换。此外接口类型的的解析器函数的命名规则也与普通字段的解析器函数命名规则不同,但是同样严厉。接口的解析器函数名必须是To加实际类型的名字,比如说这里Actor类型实现了Person接口,那么该接口的解析器函数名必须是ToActor

type person interface {
	ID() graphql.ID
	Name() string
	Birthday() string
	Sex() string
}

type personResolver struct { // Person的解析器
	person
}
// Person的解析器函数
func (r personResolver) ToActor() (*actorResolver, bool) {
	p, ok := r.person.(*actorResolver)
	return p, ok
}
// Person的另一个解析器函数
func (r personResolver) ToDirector() (*directorResolver, bool) {
	p, ok := r.person.(*directorResolver)
	return p, ok
}

请注意,这里两个解析器函数的接收者都是值类型,而不是前面例子中的指针类型。先记住这一点,每次这么说的时候都没啥好事,正所谓无坑不强调。

film结构体当然也需要响应的变化,同时还需要一些额外的数据。

type film struct {
	ID       graphql.ID
	Name     string
	Country  string
	Director director // 导演
	Stars    []actor  // 演员
	Style    []string
	Year     int32
	Runtime  int32
	Color    bool
	Score    float64
}

var Jiang_Wen = director{"nm0422638", "姜文", "1963-01-05", "男", "北京不亦乐乎电影文化发展有限公司"}
var Peng_Yuyan = actor{"nm2108643", "彭于晏", "1982-03-24", "男", "weibo.com/eddiepeng"}
var Liao_Fan = actor{"nm1233224", "廖凡", "1974-02-14", "男", "http://weibo.com/fanliao"}
var Zhou_Yun = actor{"nm1497874", "周韵", "1978-12-17", "女", ""}
var Xu_Qing = actor{"nm0944647", "许晴", "1969-01-22", "女", ""}

var Hidden_Man = &film{
	ID:       "tt8434380",
	Name:     "邪不压正",
	Country:  "China",
	Director: Jiang_Wen,
	Stars:    []actor{Peng_Yuyan, Liao_Fan, Zhou_Yun, Xu_Qing},
	Style:    []string{"DRAMA", "COMEDY", "ACTION"},
	Year:     2018,
	Runtime:  137,
	Color:    true,
	Score:    7.1,
}

在之前的Schema中,演员字段还是返回的字符串列表,只能查询演员的名字。现在是时候改变一下了,我们要查询真正的演员,要获得更多关于他们的信息。

将Schema中film对象的stars字段修改如下。

type Film {
    ... ...
    stars : [Person!]!
    ... ...
}

这里依然用了两个!限定列表和列表中的值都为非空。那么为什么查询演员返回的却是Person类型的列表,而不直接返回Actor类型的列表呢?那样不是更方便吗?

问题在于导演姜文同样也是主演之一,如果返回Actor类型,那么就会漏掉姜文。所以在这里我们返回Person类型的列表,由于Director也实现了Person类型,这样就可以查询到所有的主演了。

stars字段的解析器函数也要做出修改,返回personResolver,也就是接口解析器类型的切片。由于actorResolverdirectorResolver都实现了Person接口,所以,通过personResolver也能查询到演员和导演的信息。

func (r *filmResolver) Stars() []personResolver {
	resolvers := []personResolver{personResolver{&directorResolver{&r.f.Director}}}
	for i := 0; i < len(r.f.Stars); i++ {
		resolvers = append(resolvers, personResolver{&actorResolver{&r.f.Stars[i]}})
	}
	return resolvers
}

一切就绪,安排~

我们可以通过下面的查询查询到主演们的编号、姓名、生日和性别。

{
    film {
        stars {
            id
            name
            birthday
            sex
        }
    }
}

在这里插入图片描述

查询接口的公共字段完全没问题,可是,如果我想同时查询导演的公司和演员的主页呢?直接将companywebsite字段列在查询的后面肯定是行不通的。因为对于Director类型它无法解释website是个什么鬼,Actor类型也是一样。那难道就一点办法没有了吗?

当然有办法,这需要用到GraphQL的内联片段。它是一种查询语法,是专门针对接口类型和联合类型的查询手段,联合类型会在下一章介绍。

接口类型可以代表不同的实际类型,而不同的实际类型又有各自不同的特色字段。那么在查询这些特色字段时就需要一种选择机制,当接口表示实际类型A的时候,查询A的特色字段,代表实际类型B时,就查询B的特色字段,就像switch或者if-else一样。这种机制就叫做内联片段,语法是用... on 实际类型名 {特色字段}包裹在特色字段。

对于我们的例子,可以用下面查询来获得主演们的详细信息。

{
  film {
    stars {
      id
      name
      birthday
      sex
      ... on Actor {
        website
      }
      ... on Director {
        company
      }
    }
  }
}

在这里插入图片描述

查询成功,也是时候算算账了。还记得前面让你记住的地方吗?personResolver的方法用的是值接收者,而不是像其他解析器一样用指针接收者。

  • 问题:personResolver的方法能否改为指针接收者?为什么?

答案是不能!因为stars字段的解析器函数Stars( )函数的返回值是[]personResolver类型,而不是[]*personResolver类型。

你说肯定啊,因为Schema中stars字段的类型是[Person!]!啊,Person后面不加!才应该是[]*personResolver类型嘛。

这是没有错的,但同时这也是问题的答案是不能的原因。那么[]personResolver类型和[]*personResolver类型有什么区别呢?

其实上面说的只是浅层次的原因,深层次的原因是在Golang中,值类型只拥有以值作为接收者的方法,而指针类型却同时拥有以值作为接收者的方法和以指针作为接收者的方法。因为指针总是可以解引用,而值却不一定总是能取地址。更加详细的内容可以移驾☞此处

当我们的Stars( )函数返回[]personResolver类型时,缔结契约的解析系统会去找personResolver值类型上的解析器函数ToActorToDirector。这时如果我们以指针作为接受者来定义这两个方法,那么解析系统是铁定找不到这两个函数的,因为这两个方法是属于*personResolver类型的,而不属于personResolver类型。找不到这两个函数也就无法完成接口类型到实际类型的转换,系统就会向你抱怨说无法完成转换。

而当我们的Stars( )函数返回[]*personResolver类型时,缔结契约的解析系统就会到*personResolver类型上去找解析器函数ToActorToDirector。这时不管这两个方法的接收者是什么类型,系统都能找到它们。所以这种写法是更安全保险的,我这里只是为了引出这个问题才这么写。另外别忘了,这时还要把stars字段的类型改成[Person]!类型,同时Stars函数也要做出相应的修改。

func (r *filmResolver) Stars() []*personResolver {
	resolvers := []*personResolver{&personResolver{&directorResolver{&r.f.Director}}}
	for i := 0; i < len(r.f.Stars); i++ {
		resolvers = append(resolvers, &personResolver{&actorResolver{&r.f.Stars[i]}})
	}
	return resolvers
}

联合类型

查询接口类型的效果就是可以返回几个不同类型中的一个,只不过这些类型有一些公共字段。从效果上来说就好像是把几种不同的类型捆绑在一起了,条件是这些类型必须继承至同一个接口,比如你可以把海飞丝和飘柔捆绑在一起销售,但是不能把海飞丝和舒肤佳捆绑在一起卖给客户。如果非要把海飞丝和舒肤佳捆绑在一起行不行呢?

答案是行的!这就要用到联合类型,从各方面看,他都非常像接口类型。但他却不要求捆绑的类型必须有公共字段,甚至可以没有任何联系,即使是把海飞丝和茄子捆绑在一起也没问题。

联合类型用union关键字定义,对象之间|分隔。以前面的演员类型和导演类型为例,我们可以将它们组合成一个联合类型。

union Actor_Director = Actor | Director

需要注意的是被联和的类型必须是对象类型,不能是标量类型;其次必须是具体类型,不能是接口类型或另一个联合类型。因为接口类型和联合类型代表着一种不确定的类型,你不能用一个不确定的类型来创建另一个不确定的类型,那样只会更不靠谱。

让我们接着上一章的例子,用Actor类型和Director类型来创建联合类型。你会看到联合类型和接口类型是多么的相似,但却简单很多。

首先我们需要在Schema中定义一个联合类型,并修改Film对象的stars字段,让它返回联合类型的列表,而不再是接口类型了。

var s = `
		... ...
		type Film {
			... ...
			stars  : [Actor_Director]!
			... ...
		}
		... ...
		# 联合类型
		union Actor_Director = Actor | Director
`

联合类型的解析器以及解析器函数和接口类型一模一样,就连解析器函数的命名规则也一样。被联合的类型有几个,就需要几个解析器函数,它们的作用也是将联合类型转换成具体类型。

type unionResolver struct { //联合类型解析器
	u interface{}
}
//解析器函数
func (r *unionResolver) ToActor() (*actorResolver, bool) {
	a, ok := r.u.(*actorResolver)
	return a, ok
}
//解析器函数
func (r *unionResolver) ToDirector() (*directorResolver, bool) {
	d, ok := r.u.(*directorResolver)
	return d, ok
}

最后还需要对satrs字段的解析器函数做出修改。

func (r *filmResolver) Stars() []*unionResolver {
	resolvers := []*unionResolver{&unionResolver{&directorResolver{&r.f.Director}}}
	for i := 0; i < len(r.f.Stars); i++ {
		resolvers = append(resolvers, &unionResolver{&actorResolver{&r.f.Stars[i]}})
	}
	return resolvers
}

ToActor函数和ToDirector函数是用值接收者还是指针接收者以及Stars函数是返回[]unionResolver类型还是返回[]*unionResolver类型,这些都在上一章详细说明了,这里也是同样的道理。

再强调一次,虽然这里联合的Actor类型和Director类型都继承自同一接口,但是联合类型不要求被联合的类型之间有任何关系。

现在让我们来查询一下主演们,如果是导演,就显示姓名、生日和公司,如果是演员就显示姓名和主页。

{
    film {
        stars {
            ... on Actor {
                name
                website
            }
            ... on Director {
                name
                birthday
                company
            }
        }
    }
}

结果如图☟
在这里插入图片描述

到目前为止,我们已经学习了GraphQL类型系统的大部分类型,还剩输入类型没有介绍,因为现在还不到他发挥威力的时候。在真正学习输入类型之前,我们还需要先学习一些其他知识作为铺垫。

参数

在前面的例子中,我们已经进行过很多次GraphQL风格的查询了,但是查询过程却不完全可控。虽然我们能选择查询的字段,但是却不能选择查询的对象。因为之前只有一部电影的数据,也没有可选择的余地,但是一旦数据丰富起来,我们就会有另一个需求,我们希望按照类型或者名字甚至是ID号来查询特定的电影信息。

打个比方,如果超市只卖海飞丝,那么你跟销售员说“我要买洗发水”,和你说“我要买海飞丝的洗发水”是一个意思。销售员会拿给你海飞丝的洗发水,绝不会搞错。但是如果现在超市又进购了飘柔,你说“我要买洗发水”,这时销售就会疑惑了,他不知道究竟是该给你海飞丝还是该给你飘柔。所以现在你需要告诉销售员,我需要的是飘柔。

“飘柔”就是你给销售员的一个参数,通过这个参数她就能准确无误的把飘柔拿给你了。GraphQL的查询同样支持参数,通过在查询时给定参数,进一步过滤数据,返回客户端真正想要的数据。

那么参数该写在那里呢?

学编程的都知道,参数是跟在函数后面的。在SDL语言中,参数是跟在字段后面的。无论是继承的特性还是可以带参数,怎么看,SDL的字段都更像是函数,虽然它被称为字段。

让我们来看看参数该怎么玩。现在让我们实现这样一个功能,查询某一类型的所有电影。

首先我们需要更多的数据,我们已经有了《邪不压正》这部电影的信息,现在我们将《我不是药神》也加进来,并把它们放到一个电影切片中。

var Wen_Muye = director{"nm6337063", "文牧野", "1985", "男", ""}
var Xu_Zheng = actor{"nm1905770", "徐峥", "1972-04-18", "男", ""}
var Wang_Chuanjun = actor{"nm4369372", "王传君", "1985-10-18", "男", ""}
var Zhou_Yiwei = actor{"nm2489423", "周一围", "1982-08-24", "男", ""}
var Tan_Zhuo = actor{"nm3431007", "谭卓", "1983-09-25", "女", ""}
var Zhang_Yu = actor{"nm9636805", "章宇", "1982", "男", "weibo.com/feishe92806"}

var WoBuShiYaoShen = &film{
	ID:       "tt7362036",
	Name:     "我不是药神",
	Country:  "中国大陆",
	Director: Wen_Muye,
	Stars:    []actor{Xu_Zheng, Wang_Chuanjun, Zhou_Yiwei, Tan_Zhuo, Zhang_Yu},
	Style:    []string{"DRAMA", "COMEDY"},
	Year:     2018,
	Runtime:  117,
	Color:    true,
	Score:    8.9,
}

var films = []*film{Hidden_Man, WoBuShiYaoShen} //电影切片,包含所所有的电影数据信息

然后我们需要给Schema的Query对象添加一条带参数的查询字段flmWithStyle。参数写在紧跟字段的小括号中,这和声明一个函数并无区别。

var s = `
		... ...
        type Query {
        	... ...
        	filmWithStyle(style :Style!) :[Film]!
        }
		... ...
`

字段filmWithStyle有一个Style类型的参数,这是一个枚举类型,这才是使用枚举类型的正确姿势。参数类型后面也可以加!来限制参数不能为空,整个字段返回[Film]!类型的数据,也就是电影的切片。

既然有了新的字段,就需要为它编写解析器函数。它是Query对象的字段,那么它的解析器函数也应该是根解析器Resolver的方法。

之前写的解析器函数都是不带参数的,现在filmWithStyle字段的解析器函数需要有一个参数。对应到Golang中,解析器函数的参数必须是结构体类型。一般都会使用匿名结构体,匿名结构体的每个字段对应一个真正的参数。

func (r *Resolver) FilmWithStyle(args struct{ Style string }) []*filmResolver {
	resolvers := []*filmResolver{}
	for i := 0; i < len(films); i++ {
		for _, v := range films[i].Style {
			if args.Style == v {
				resolvers = append(resolvers, &filmResolver{films[i]})
				break
			}
		}
	}
	return resolvers
}

没有更多需要修改的地方了,现在我们可以查询某一类型的电影了,安排~

在查询的时候,我们同样需要提供查询的参数。比如说,我们要查询喜剧片,那么可以这样查询。

{
  filmWithStyle(style: COMEDY) {
    name
    style
  }
}

参数写在紧跟字段的小括号中,并且只能是以具名的方式给出,就跟函数调用一样。上面的查询结果如下图。
在这里插入图片描述

试着查询动作片,看看等得到什么结果。
在这里插入图片描述


高级查询:变量

有没有发现一个问题?上面的例子中,每次想查询不同类型电影时都要重新输入filmWithStyle的参数。你说这不是废话吗?当然要重新输入参数了。

虽然提供了参数,但还是太麻烦了。filmWithStyle就相当于一个函数,而我们现在的做法就是把参数给写死了,这样函数就完全失去它本该发挥的作用了。仔细一分析你就会发现,这样的做法是不对的。无论哪一门语言,函数的参数都是外边带入的,可随意变化的。

那么有没有办法把filmWithStyle的参数也变成一个参数呢?GraphQL是支持这么做的。

一直以来我们都使用了简写版的查询语句,即省略了query,现在需要把query关键字加上。另外,查询是可以命名的,就像定义变量一样。把query也看做是一种结构体类型,那么我们以前使用的都是匿名结构体,现在我们需要给这个“结构体”变量取一个名字了。

query Film {
    filmWithStyle(style: ACTION) {
        name
        style
    }
}

我们给查询取了一个名字叫Film,但是看起来还是没有什么本质的变化。但是不要眨眼,下面神奇的事情就发生了。

query Film($param :Style!) {
  filmWithStyle(style: $param) {
    name
    style
  }
}

我们在Film的后面也声明了一个参数$param,它是Style类型的,并且不能为空。然后将$param作为参数传递给了filmWithStyle字段,现在它的参数不再是一个静态值,而是动态的。这样,客户端只要给出不同的参数就能查询不同的内容了,而不需要重新构建一个查询。

等等,牛皮倒是吹的挺响,$param不还是一个参数吗?真正的值又该怎么传递给它呢?这就要看轮子的质量好不好了,既然费劲造了轮子,现在是得到回报的时候了。
在这里插入图片描述

广告说的好,哪里不会点哪里。点完之后你会看到多出来一块输入区,那里就是输入参数的地方。
在这里插入图片描述

参数完全是Json的格式,对于本例而言,参数如下。

{
    "param" : "ACTION"
}

在次查询动作类电影,可以得到和之前一样的结果。
在这里插入图片描述

更加惊喜的是,参数还可以有默认值,唯一的条件就是有默认值时,参数不能是非空类型,也即是不能有!,它和默认值是不共戴天的。

query Film($param :Style = COMEDY) {
  filmWithStyle(style: $param) {
    name
    style
  }
}

关于高级查询还有很多内容,到目前为止介绍过的高级查询只有内联片段和参数,有关高级查询的更多内容将在后面再介绍。

变更

变更就是用来更新数据的。在对象与字段一章中,提到过schemamutation字段,他相当于REST的POST。变更就是通过mutation字段实现的,本章将介绍该字段的使用。

一部电影怎么少得了影评呢?这就加上影评。不过这次我们不是先写好影评数据,然后去查询,这次我们来通过mutation添加影评,当然查询影评的功能也是必须的。

第一步是准备好数据,为了简单起见,影评只包含两项内容。评论人和评论。影评结构体定义如下。

type Review struct {
    Name    string
    comment string
}

然后在film结构体中加上Reviews字段。

type film struct {
    ... ...
    Reviews []Review
}

之前使用的两个电影结构体数据也要加上Reviews,只不过我们给的是空切片,没有实际的数据,影评将通过mutation来添加。

var Hidden_Man = &film {
    ... ...
    Reviews: []Review{},
}

var WoBuShiYaoShen = &film {
    ... ...
    Reviews: []Review{},
}

在Schema中也要添加Review的定义。

var s = `
		... ...
        type Review {
        	name    : String!
        	comment : String!
        }
`

在实现影评的添加功能之前,先来实现以下查询影评的功能。首先需要给Film对象添加reviews字段,这样才能进行查询。

var s = '
		... ...
        type Film {
            ... ...
            reviews: [Review]
        }
		... ...
'

接下来要编写Review对象的解析器以及它的每个字段的解析器函数。

type reviewResolver struct {
	r *Review
}

func (r *reviewResolver) Name() string {
	return r.r.Name
}

func (r *reviewResolver) Comment() string {
	return r.r.Comment
}

最后是reviews字段的解析器函数。

func (r *filmResolver) Reviews() *[]*reviewResolver {
	reviews := []*reviewResolver{}
	for i := 0; i < len(r.f.Reviews); i++ {
		reviews = append(reviews, &reviewResolver{&r.f.Reviews[i]})
	}
	return &reviews
}

这一套流程我希望你应该是很熟悉了的。

如果现在去查询评论,无疑不会有任何结果。要完成添加影评的功能,首先需要定义一个Mutation对象。

var s = `
        schema{
            query    : Query
            mutation : Mutation
        }
        type Mutation {
            addReview(id :ID!, name :String!, comment :String!) :Review
        }
`

Query对象一样,Mutation对象的名字也可以随便起,但习惯上就叫Mutation了。Mutation对象也有一个字段addReview,它就是用来添加影评的,影评内容将通过参数传递进来。最后它还会返回一个Review对象,也就是被添加的评论本身,这不是必须的,也是经验使然,这样在添加完影评之后立马就能看到添加对了没有。

addReview字段的参数中,我们依然保留了id,因为我们需要知道评论是属于哪部电影的。Mutation也是挂在根解析器上的,因此addReview的解析器函数也是根解析器的方法。

func (r *Resolver) AddReview(args struct {
	ID      graphql.ID
	Name    string
	Comment string
}) *reviewResolver {
	new_review := Review{args.Name, args.Comment}
	for i := 0; i < len(films); i++ {
		if films[i].ID == args.ID {
			films[i].Reviews = append(films[i].Reviews, new_review)
			return &reviewResolver{&new_review}
		}
	}
	return nil
}

让我们来为《邪不压正》添加一条影评。

mutation {
  addReview(id:"tt8434380", name:"小妮子", comment:"姜文的民国三部曲终章!") {
    name
    comment
  }
}

如果一切正常,你将看到下面的结果。
在这里插入图片描述

唯一的遗憾就是数据没有存到数据库,而是在内存中,下次运行就没了,后面我们在解决这个问题。

输入类型

上一章介绍了如何利用mutation来添加影评,我们将影评所需的用户名和评论内容作为参数传递给了addReview字段。这样做虽然能正常工作,但是,却让人疑惑,如果不是自己写的代码,根本不知道这两条数据代表的是什么。

现在我们有了这样一种述求,在利用mutation变更时,希望能够传递一个结构化的对象,而不是一群散兵。

GraphQL提供了input类型来满足这一述求。inputtype这两个关键字就像孪生兄弟一样,使用方法和语法也一样。input关键字也是用来定义对象的,只不过它定义的对象用在mutation中作为输入参数,而type定义的对象多用于查询。

由于input定义的对象不能用于查询,因此它定义的对象也不需要有解析器,只需要在Golang中有一个对应的结构体类型就行了。

现在我们将上一章添加评论的功能改一改,使用输入类型作为参数。首先需要在Schema中定义一个输入类型,它和Review类型的结构是一模一样的。

var s = `
		... ...
        input ReviewInput {
        	Name 	: String!
        	Comment : String!
        }
`

同时Mutation对象的addReview字段也需要做出修改,我么使用IDReviewInput类型作为参数。

var s = `
		... ...
        type Mutation {
        	addReview(id :ID!, review :ReviewInput!) :Review
        }
		... ...
`

ReviewInput对象在Golang中需要有一个对应的结构体。

type ReviewInput struct {
	Name    string
	Comment string
}

最后是修改addReview字段的解析器函数,使用ReviewInput类型作为参数,其他还和以前一样。

func (r *Resolver) AddReview(args struct {
	ID      graphql.ID
	Review  *ReviewInput //使用ReviewInput类型作为参数
}) *reviewResolver {
	new_review := Review{args.Review.Name, args.Review.Comment}
	for i := 0; i < len(films); i++ {
		if films[i].ID == args.ID {
			films[i].Reviews = append(films[i].Reviews, new_review)
			return &reviewResolver{&new_review}
		}
	}
	return nil
}

现在可以用下面的方式来添加一条影评。

mutation {
  addReview(id:"tt8434380", review:{name:"小妮子",comment:"姜文的民国三部曲终章!"}) {
    name
    comment
  }
}

在这里插入图片描述

现在我们就知道第二个参数代表的是一条影评了。这样做的正确的,但姿势还不够优美。前面曾介绍过如何用变量来进行高级查询。在mutation中同样可以使用变量,方式和查询是一样的。

首先还是要给变更取一个名字,然后写入参数。

mutation AddReview($id :ID!, $review :ReviewInput!){
  addReview(id: $id, review: $review) {
    name
    comment
  }
}

参数如下,依然是Json格式的。

{
  "id": "tt8434380",
  "review": {
    "name": "大脸蛋",
    "comment": "姜文的民国三部曲终章!"
  }
}

结果如下图。
在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值