多年来,人们一直在使用REST API来满足开发需求,但得完成大量不必要的调用后,开发者才能灵活使用。例如,如果Web和移动设备所需的数据不同,我们还须针对Web和移动设备创建两个不同的端点。
因此,Facebook创建了一种查询语言——GraphQL,该语言可以准确地给出开发者查询的内容,干净利落,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
本文将重点介绍GraphQL的主要功能,以及就API而言它存在的优缺点。文末将展示一个使用Golang的简单程序(已搭建GraphQL)。
什么是GraphQL?
GraphQL是用于API的查询语言,它是服务器端运行时,通过为数据定义的类型系统执行查询。
GraphQL是一种查询语言,适用许多领域,但通常用来在客户端和服务器应用程序之间搭桥。无所谓使用的是哪个网络层,所以可以在客户端和服务器应用程序之间读取和写入数据。(RobinWieruch《GraphQL指南》)
虽然GraphQL是查询语言,但它与数据库没有直接关系,也就是GraphQL不限于任意SQL或是NoSQL的数据库。GraphQL位于客户端和服务器端,通过API连接/访问。开发这种查询语言的目的之一是通过提供所需的数据来促进后端、前端或移动应用程序之间的数据通信。
GraphQL的操作
1. 查询(Query)
查询用于读取或获取值。无论哪种情况,操作都是一个简单的字符串,GraphQL服务器可以解析该字符串并以特定格式的数据进行响应。
你可以使用查询操作从API请求数据。查询描述需要从GraphQL服务器获取的数据,发送查询其实是按字段要求提取数据。(Eve Porcello、Alex Banks著《学习GraphQL》)
2. 模式(Schema)
GraphQL使用Schema描述数据图的形状。这样的Schema定义类型的层次结构,依托的是从后端数据存储区填充的字段,也准确表示客户端可以对数据图执行哪些查询和突变。
3. 分解器(Resolver)
分解器是负责为Schema单一字段填充数据的功能。它可以用你定义的任何方式填充该数据,例如从后端数据库或第三方API提取数据。
4. 突变(Mutation)
修改数据存储中的数据并返回一个值,它可用于插入、更新或删除数据。
突变与查询原理相同:它具有字段和对象、参数和变量、片段和操作名称,以及返回结果的指令和嵌套对象。(Robin Wieruch著《GraphQL之路》)
5. 订阅(Subscription)
将数据从服务器推送到客户端的方法是选择侦听来自服务器的实时消息。
GraphQL的订阅来自Facebook的真实用例。开发团队希望找到一种方法,不刷新页面就能实时显示发文获得的有效点赞(Live Likes)。(Eve Porcello、Alex Banks著《学习GraphQL》)
GraphQL的优势与劣势
1. 优势
(1) 开发迅速
来看一个案例:如何得到图书借阅者的数据。在视图中,首先我要显示书籍列表,书籍列表菜单显示中出现一个借阅者的列表。在REST API中,需要创建新的端点以返回图书清单,再创建一个新的端点以返回每本书的借阅人。
与REST API不同,GraphQL中仅使用一个端点就可以返回书籍列表和借阅者列表了。
使用以下示例GraphQL查询:
(2) 灵活性
来看一个案例:如何获取书籍详细信息。在网络视图上,我想展示书籍详细信息,例如名称、价格和介绍。在REST API中需要创建一个新的端点以返回名称、价格、介绍等的书籍详细信息。
如果在移动端查看时,只想展示图书详细信息中的名称和价格怎么办?如果使用与Web视图相同的端点,则会浪费介绍的数据。所以需要更改该端点内部的现有逻辑,或创建一个新的端点。
与REST API不同,GraphQL中仅使用一个端点即可按照Web或移动设备的需求返回书籍详细信息。在GraphQL中,只需更改查询。
(3) 维护简单,易于使用
- Rest API:如果客户端需要其他数据,通常需要添加一个新端点或更改一个现有端点。
- GraphQL:客户只需要更改查询。
2. 缺点
- 处理文件上传:GraphQL规范中没有关于文件上传的内容,并且突变不接受参数中的文件。
- 简单的API:如果你的API非常简单,那GraphQL只会使其复杂,所以使用REST API可能会更好。
代码实现
实现过程使用了Golang编程语言,这里是项目架构:
在依赖版本和依赖管理功能上使用的是go模块。用graphql-go来支持查询、突变和订阅;用graphql-go-handler来支持处理器。此时,我将创建一个简单的程序,这里使用GraphQL为详细书目创建CRUD。步骤如下:
先新建一个环境文件夹,然后新建一个名为connection.yml的文件:
- app:
- name: "GraphQL Test"
- debug: true
- port: "8080"
- host: "localhost"
- service: "http"
- context:
- timeout: 2
- databases:
- mongodb:
- name: "local_db"
- connection: "mongodb://root:root@localhost:27017"
然后创建一个架构文件夹,创建名为databaseConfiguration.go、environmentConfiguration.go和model.go的文件。这个文件夹用来配置数据库并从connection.yml读取数据。
(1) databaseConfiguration.go
- package infrastructureimport(
- "context"
- "go.mongodb.org/mongo-driver/mongo"
- "go.mongodb.org/mongo-driver/mongo/options"
- "log"
- )var Mongodb *mongo.Databasefunc(e *Environment) InitMongoDB()(db *mongo.Database, err error) {
- clientOptions :=options.Client().ApplyURI(e.Databases["mongodb"].Connection)
- client, err := mongo.Connect(context.TODO(),clientOptions)
- err = client.Ping(context.TODO(), nil)
- if err != nil {
- return db, err
- }
- Mongodb = client.Database(e.Databases["mongodb"].Name)
- log.Println("Mongodb Ready!!!")
- return db, err
- }
(2) environmentConfiguration.go
- package infrastructureimport(
- "io/ioutil"
- "log"
- "os"
- "path"
- "runtime""gopkg.in/yaml.v2"
- )func(env *Environment) SetEnvironment() {
- _, filename, _, _ := runtime.Caller(1)
- env.path = path.Join(path.Dir(filename),"environment/Connection.yml")
- _, err := os.Stat(env.path)
- if err != nil {
- panic(err)
- return
- }
- }func(env *Environment) LoadConfig() {
- content, err :=ioutil.ReadFile(env.path)
- if err != nil {
- log.Println(err)
- panic(err)
- }
- err =yaml.Unmarshal([]byte(string(content)), env)
- if err != nil {
- log.Println(err)
- panic(err)
- }
- if env.App.Debug == false {
- log.SetOutput(ioutil.Discard)
- }
- log.Println("Config load successfully!")
- return
- }
(3) model.go
- package infrastructuretypeapp struct{
- Appname string `yaml:"name"`
- Debug bool `yaml:"debug"`
- Port string `yaml:"port"`
- Service string `yaml:"service"`
- Host string `yaml:"host"`
- }type database struct {
- Name string `yaml:"name"`
- Connection string`yaml:"connection"`
- }type Environment struct {
- App app `yaml:"app"`
- Databases map[string]database`yaml:"databases"`
- path string
- }
第三,创建一个书目文件夹,创建如下文件:
model.go:
- package
- package booktypeBook struct {
- Name string
- Price string
- Description string
- } booktypeBook struct { Name string Price string Description string}
resolver.go:
- package bookimport(
- "context""github.com/graphql-go/graphql"
- )var productType = graphql.NewObject(
- graphql.ObjectConfig{
- Name: "Book",
- Fields: graphql.Fields{
- "name": &graphql.Field{
- Type: graphql.String,
- },
- "price":&graphql.Field{
- Type: graphql.String,
- },
- "description":&graphql.Field{
- Type: graphql.String,
- },
- },
- },
- )var queryType = graphql.NewObject(
- graphql.ObjectConfig{
- Name: "Query",
- Fields: graphql.Fields{
- "book":&graphql.Field{
- Type: productType,
- Description: "Get bookby name",
- Args: graphql.FieldConfigArgument{
- "name":&graphql.ArgumentConfig{
- Type: graphql.String,
- },
- },
- Resolve: func(pgraphql.ResolveParams) (interface{}, error) {
- var result interface{}
- name, ok :=p.Args["name"].(string)
- if ok {
- // Find product
- result =GetBookByName(context.Background(), name)
- }
- return result, nil
- },
- },
- "list":&graphql.Field{
- Type: graphql.NewList(productType),
- Description: "Get booklist",
- Args: graphql.FieldConfigArgument{
- "limit":&graphql.ArgumentConfig{
- Type: graphql.Int,
- },
- },
- Resolve: func(paramsgraphql.ResolveParams) (interface{}, error) {
- var result interface{}
- limit, _ :=params.Args["limit"].(int)
- result =GetBookList(context.Background(), limit)
- return result, nil
- },
- },
- },
- })var mutationType =graphql.NewObject(graphql.ObjectConfig{
- Name: "Mutation",
- Fields: graphql.Fields{
- "create":&graphql.Field{
- Type: productType,
- Description: "Create newbook",
- Args: graphql.FieldConfigArgument{
- "name":&graphql.ArgumentConfig{
- Type:graphql.NewNonNull(graphql.String),
- },
- "price":&graphql.ArgumentConfig{
- Type:graphql.NewNonNull(graphql.String),
- },
- "description":&graphql.ArgumentConfig{
- Type:graphql.NewNonNull(graphql.String),
- },
- },
- Resolve: func(paramsgraphql.ResolveParams) (interface{}, error) {
- book := Book{
- Name: params.Args["name"].(string),
- Price: params.Args["price"].(string),
- Description:params.Args["description"].(string),
- }
- if err := InsertBook(context.Background(), book); err != nil {
- return nil, err
- }return book, nil
- },
- },"update":&graphql.Field{
- Type: productType,
- Description: "Update bookby name",
- Args: graphql.FieldConfigArgument{
- "name":&graphql.ArgumentConfig{
- Type:graphql.NewNonNull(graphql.String),
- },
- "price":&graphql.ArgumentConfig{
- Type: graphql.String,
- },
- "description":&graphql.ArgumentConfig{
- Type: graphql.String,
- },
- },
- Resolve: func(paramsgraphql.ResolveParams) (interface{}, error) {
- book := Book{}
- if name, nameOk := params.Args["name"].(string); nameOk {
- book.Name = name
- }
- if price, priceOk := params.Args["price"].(string); priceOk {
- book.Price = price
- }
- if description, descriptionOk :=params.Args["description"].(string); descriptionOk {
- book.Description = description
- }if err :=UpdateBook(context.Background(), book); err != nil {
- return nil, err
- }
- return book, nil
- },
- },"delete": &graphql.Field{
- Type: productType,
- Description: "Delete bookby name",
- Args: graphql.FieldConfigArgument{
- "name":&graphql.ArgumentConfig{
- Type:graphql.NewNonNull(graphql.String),
- },
- },
- Resolve: func(paramsgraphql.ResolveParams) (interface{}, error) {
- name, _ :=params.Args["name"].(string)
- if err := DeleteBook(context.Background(), name); err != nil {
- return nil, err
- }
- return name, nil
- },
- },
- },
- })// schema
- var Schema, _ = graphql.NewSchema(
- graphql.SchemaConfig{
- Query: queryType,
- Mutation: mutationType,
- },
- )
repository.go:
- package bookimport(
- "context"
- "log""graphql/infrastructure""go.mongodb.org/mongo-driver/bson"
- "go.mongodb.org/mongo-driver/mongo/options"
- )funcGetBookByName(ctxcontext.Context, name string) (result interface{}){
- var book Book
- data :=infrastructure.Mongodb.Collection("booklist").FindOne(ctx,bson.M{"name": name})
- data.Decode(&book)
- return book
- }funcGetBookList(ctxcontext.Context, limit int) (result interface{}){
- var book Book
- var books []Bookoption := options.Find().SetLimit(int64(limit))cur, err:= infrastructure.Mongodb.Collection("booklist").Find(ctx, bson.M{},option)
- defer cur.Close(ctx)
- if err != nil {
- log.Println(err)
- return nil
- }
- for cur.Next(ctx) {
- cur.Decode(&book)
- books = append(books, book)
- }
- return books
- }funcInsertBook(ctxcontext.Context, book Book) error {
- _, err :=infrastructure.Mongodb.Collection("booklist").InsertOne(ctx, book)
- return err
- }funcUpdateBook(ctxcontext.Context, book Book) error {
- filter := bson.M{"name":book.Name}
- update := bson.M{"$set":book}
- upsertBool := true
- updateOption := options.UpdateOptions{
- Upsert: &upsertBool,
- }
- _, err :=infrastructure.Mongodb.Collection("booklist").UpdateOne(ctx, filter,update, &updateOption)
- return err
- }funcDeleteBook(ctxcontext.Context, name string) error {
- _, err :=infrastructure.Mongodb.Collection("booklist").DeleteOne(ctx,bson.M{"name": name})
- return err
- }
response.go:
- package bookimport(
- "encoding/json"
- "net/http"
- "time"
- )type SetResponsestruct {
- Status string `json:"status"`
- Data interface{} `json:"data,omitempty"`
- AccessTime string `json:"accessTime"`
- }funcHttpResponseSuccess(w http.ResponseWriter, r *http.Request, data interface{}){
- setResponse := SetResponse{
- Status: http.StatusText(200),
- AccessTime: time.Now().Format("02-01-2006 15:04:05"),
- Data: data}
- response, _ :=json.Marshal(setResponse)
- w.Header().Set("Content-Type", "Application/json")
- w.WriteHeader(200)
- w.Write(response)
- }funcHttpResponseError(w http.ResponseWriter, r *http.Request, data interface{},code int) {
- setResponse := SetResponse{
- Status: http.StatusText(code),
- AccessTime: time.Now().Format("02-01-2006 15:04:05"),
- Data: data}
- response, _ :=json.Marshal(setResponse)
- w.Header().Set("Content-Type", "Application/json")
- w.WriteHeader(code)
- w.Write(response)
- }
routes.go:
- package bookimport(
- "github.com/go-chi/chi"
- "github.com/go-chi/chi/middleware"
- "github.com/graphql-go/handler"
- )funcRegisterRoutes(r *chi.Mux) *chi.Mux {
-
- graphQL := handler.New(&handler.Config{
- Schema: &Schema,
- Pretty: true,
- GraphiQL: true,
- })
- r.Use(middleware.Logger)
- r.Handle("/query", graphQL)
- return r
- }
最后,创建名为 main.go的文件。
main.go:
- package mainimport(
- "github.com/go-chi/chi"
- "graphql/book"
- "graphql/infrastructure"
- "log"
- "net/http"
- "net/url"
- )funcmain() {
- routes := chi.NewRouter()
- r := book.RegisterRoutes(routes)
- log.Println("Server ready at 8080")
- log.Fatal(http.ListenAndServe(":8080", r))
- }funcinit() {
- val := url.Values{}
- val.Add("parseTime", "1")
- val.Add("loc", "Asia/Jakarta")
- env := infrastructure.Environment{}
- env.SetEnvironment()
- env.LoadConfig()
- env.InitMongoDB()
- }
运行程序的结果如下:
GraphQL有很多优点,但事实证明,与REST API相比,GraphQL处理文件上传和简单API的性能表现有所不足。因此,我们必须首先了解要构建的系统,是否适合将GraphQL用作应用程序的设计架构。