Facebook:如何在Golang中搭建GraphQL?
全文共9939字 , 预计学习时长25分钟

文章图片
多年来 , 人们一直在使用RESTAPI来满足开发需求 , 但得完成大量不必要的调用后 , 开发者才能灵活使用 。 例如 , 如果Web和移动设备所需的数据不同 , 我们还须针对Web和移动设备创建两个不同的端点 。
因此 , Facebook创建了一种查询语言——GraphQL , 该语言可以准确地给出开发者查询的内容 , 干净利落 , 也让API更容易地随着时间推移而演进 , 还能用于构建强大的开发者工具 。
本文将重点介绍GraphQL的主要功能 , 以及就API而言它存在的优缺点 。 文末将展示一个使用Golang的简单程序(已搭建GraphQL) 。
什么是GraphQL?
GraphQL是用于API的查询语言 , 它是服务器端运行时 , 通过为数据定义的类型系统执行查询 。
GraphQL是一种查询语言 , 适用许多领域 , 但通常用来在客户端和服务器应用程序之间搭桥 。 无所谓使用的是哪个网络层 , 所以可以在客户端和服务器应用程序之间读取和写入数据 。 (RobinWieruch《GraphQL指南》)
虽然GraphQL是查询语言 , 但它与数据库没有直接关系 , 也就是GraphQL不限于任意SQL或是NoSQL的数据库 。 GraphQL位于客户端和服务器端 , 通过API连接/访问 。 开发这种查询语言的目的之一是通过提供所需的数据来促进后端、前端或移动应用程序之间的数据通信 。

文章图片
GraphQL的操作
查询(Query)
查询用于读取或获取值 。 无论哪种情况 , 操作都是一个简单的字符串 , GraphQL服务器可以解析该字符串并以特定格式的数据进行响应 。
你可以使用查询操作从API请求数据 。 查询描述需要从GraphQL服务器获取的数据 , 发送查询其实是按字段要求提取数据 。 (EvePorcello、AlexBanks著《学习GraphQL》)

文章图片
模式(Schema)
GraphQL使用Schema描述数据图的形状 。 这样的Schema定义类型的层次结构 , 依托的是从后端数据存储区填充的字段 , 也准确表示客户端可以对数据图执行哪些查询和突变 。
分解器(Resolver)
分解器是负责为Schema单一字段填充数据的功能 。 它可以用你定义的任何方式填充该数据 , 例如从后端数据库或第三方API提取数据 。
突变(Mutation)
修改数据存储中的数据并返回一个值 , 它可用于插入、更新或删除数据 。
突变与查询原理相同:它具有字段和对象、参数和变量、片段和操作名称 , 以及返回结果的指令和嵌套对象 。 (RobinWieruch著《GraphQL之路》)

文章图片
订阅(Subscription)
将数据从服务器推送到客户端的方法是选择侦听来自服务器的实时消息 。
GraphQL的订阅来自Facebook的真实用例 。 开发团队希望找到一种方法 , 不刷新页面就能实时显示发文获得的有效点赞(LiveLikes) 。 (EvePorcello、AlexBanks著《学习GraphQL》)

文章图片
GraphQL的优势与劣势

文章图片
优势
开发迅速
来看一个案例:如何得到图书借阅者的数据 。 在视图中 , 首先我要显示书籍列表 , 书籍列表菜单显示中出现一个借阅者的列表 。 在RESTAPI中 , 需要创建新的端点以返回图书清单 , 再创建一个新的端点以返回每本书的借阅人 。

文章图片
【Facebook:如何在Golang中搭建GraphQL?】与RESTAPI不同 , GraphQL中仅使用一个端点就可以返回书籍列表和借阅者列表了 。

文章图片
使用以下示例GraphQL查询:

文章图片
灵活性
来看一个案例:如何获取书籍详细信息 。 在网络视图上 , 我想展示书籍详细信息 , 例如名称、价格和介绍 。 在RESTAPI中需要创建一个新的端点以返回名称、价格、介绍等的书籍详细信息 。

文章图片
如果在移动端查看时 , 只想展示图书详细信息中的名称和价格怎么办?如果使用与Web视图相同的端点 , 则会浪费介绍的数据 。 所以需要更改该端点内部的现有逻辑 , 或创建一个新的端点 。

文章图片
与RESTAPI不同 , GraphQL中仅使用一个端点即可按照Web或移动设备的需求返回书籍详细信息 。 在GraphQL中 , 只需更改查询 。
维护简单 , 易于使用
·RestAPI:如果客户端需要其他数据 , 通常需要添加一个新端点或更改一个现有端点 。
·GraphQL:客户只需要更改查询 。
缺点
·处理文件上传:GraphQL规范中没有关于文件上传的内容 , 并且突变不接受参数中的文件 。
·简单的API:如果你的API非常简单 , 那GraphQL只会使其复杂 , 所以使用RESTAPI可能会更好 。
代码实现
实现过程使用了Golang编程语言 , 这里是项目架构:

文章图片
在依赖版本和依赖管理功能上使用的是go模块 。 用graphql-go来支持查询、突变和订阅;用graphql-go-handler来支持处理器 。 此时 , 我将创建一个简单的程序 , 这里使用GraphQL为详细书目创建CRUD 。 步骤如下:
先新建一个环境文件夹 , 然后新建一个名为connection.yml的文件:
app:name:"GraphQLTest"debug:trueport:"8080"host:"localhost"service:"http"context:timeout:2databases:mongodb:name:"local_db"connection:"mongodb://root:root@localhost:27017"
然后创建一个架构文件夹 , 创建名为databaseConfiguration.go、environmentConfiguration.go和model.go的文件 。 这个文件夹用来配置数据库并从connection.yml读取数据 。
databaseConfiguration.go
packageinfrastructureimport("context""go.mongodb.org/mongo-driver/mongo""go.mongodb.org/mongo-driver/mongo/options""log")varMongodb*mongo.Databasefunc(e*Environment)InitMongoDB()(db*mongo.Database,errerror){clientOptions:=options.Client().ApplyURI(e.Databases["mongodb"].Connection)client,err:=mongo.Connect(context.TODO(),clientOptions)err=client.Ping(context.TODO(),nil)iferr!=nil{returndb,err}Mongodb=client.Database(e.Databases["mongodb"].Name)log.Println("MongodbReady!!!")returndb,err}
environmentConfiguration.go
packageinfrastructureimport("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)iferr!=nil{panic(err)return}}func(env*Environment)LoadConfig(){content,err:=ioutil.ReadFile(env.path)iferr!=nil{log.Println(err)panic(err)}err=yaml.Unmarshal([]byte(string(content)),env)iferr!=nil{log.Println(err)panic(err)}ifenv.App.Debug==false{log.SetOutput(ioutil.Discard)}log.Println("Configloadsuccessfully!")return}
model.go
packageinfrastructuretypeappstruct{Appnamestring`yaml:"name"`Debugbool`yaml:"debug"`Portstring`yaml:"port"`Servicestring`yaml:"service"`Hoststring`yaml:"host"`}typedatabasestruct{Namestring`yaml:"name"`Connectionstring`yaml:"connection"`}typeEnvironmentstruct{Appapp`yaml:"app"`Databasesmap[string]database`yaml:"databases"`pathstring}
第三 , 创建一个书目文件夹 , 创建如下文件:

文章图片
model.go
packagepackagebooktypeBookstruct{NamestringPricestringDescriptionstring}booktypeBookstruct{NamestringPricestringDescriptionstring}
resolver.go
packagebookimport("context""github.com/graphql-go/graphql")varproductType=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,},},},)varqueryType=graphql.NewObject(graphql.ObjectConfig{Name:"Query",Fields:graphql.Fields{"book":&graphql.Field{Type:productType,Description:"Getbookbyname",Args:graphql.FieldConfigArgument{"name":&graphql.ArgumentConfig{Type:graphql.String,},},Resolve:func(pgraphql.ResolveParams)(interface{},error){varresultinterface{}name,ok:=p.Args["name"].(string)ifok{//Findproductresult=GetBookByName(context.Background(),name)}returnresult,nil},},"list":&graphql.Field{Type:graphql.NewList(productType),Description:"Getbooklist",Args:graphql.FieldConfigArgument{"limit":&graphql.ArgumentConfig{Type:graphql.Int,},},Resolve:func(paramsgraphql.ResolveParams)(interface{},error){varresultinterface{}limit,_:=params.Args["limit"].(int)result=GetBookList(context.Background(),limit)returnresult,nil},},},})varmutationType=graphql.NewObject(graphql.ObjectConfig{Name:"Mutation",Fields:graphql.Fields{"create":&graphql.Field{Type:productType,Description:"Createnewbook",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),}iferr:=InsertBook(context.Background(),book);err!=nil{returnnil,err}returnbook,nil},},"update":&graphql.Field{Type:productType,Description:"Updatebookbyname",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{}ifname,nameOk:=params.Args["name"].(string);nameOk{book.Name=name}ifprice,priceOk:=params.Args["price"].(string);priceOk{book.Price=price}ifdescription,descriptionOk:=params.Args["description"].(string);descriptionOk{book.Description=description}iferr:=UpdateBook(context.Background(),book);err!=nil{returnnil,err}returnbook,nil},},"delete":&graphql.Field{Type:productType,Description:"Deletebookbyname",Args:graphql.FieldConfigArgument{"name":&graphql.ArgumentConfig{Type:graphql.NewNonNull(graphql.String),},},Resolve:func(paramsgraphql.ResolveParams)(interface{},error){name,_:=params.Args["name"].(string)iferr:=DeleteBook(context.Background(),name);err!=nil{returnnil,err}returnname,nil},},},})//schemavarSchema,_=graphql.NewSchema(graphql.SchemaConfig{Query:queryType,Mutation:mutationType,},)
repository.go
packagebookimport("context""log""graphql/infrastructure""go.mongodb.org/mongo-driver/bson""go.mongodb.org/mongo-driver/mongo/options")funcGetBookByName(ctxcontext.Context,namestring)(resultinterface{}){varbookBookdata:=infrastructure.Mongodb.Collection("booklist").FindOne(ctx,bson.M{"name":name})data.Decode(&book)returnbook}funcGetBookList(ctxcontext.Context,limitint)(resultinterface{}){varbookBookvarbooks[]Bookoption:=options.Find().SetLimit(int64(limit))cur,err:=infrastructure.Mongodb.Collection("booklist").Find(ctx,bson.M{},option)defercur.Close(ctx)iferr!=nil{log.Println(err)returnnil}forcur.Next(ctx){cur.Decode(&book)books=append(books,book)}returnbooks}funcInsertBook(ctxcontext.Context,bookBook)error{_,err:=infrastructure.Mongodb.Collection("booklist").InsertOne(ctx,book)returnerr}funcUpdateBook(ctxcontext.Context,bookBook)error{filter:=bson.M{"name":book.Name}update:=bson.M{"$set":book}upsertBool:=trueupdateOption:=options.UpdateOptions{Upsert:&upsertBool,}_,err:=infrastructure.Mongodb.Collection("booklist").UpdateOne(ctx,filter,update,&updateOption)returnerr}funcDeleteBook(ctxcontext.Context,namestring)error{_,err:=infrastructure.Mongodb.Collection("booklist").DeleteOne(ctx,bson.M{"name":name})returnerr}
response.go
packagebookimport("encoding/json""net/http""time")typeSetResponsestruct{Statusstring`json:"status"`Datainterface{}`json:"data,omitempty"`AccessTimestring`json:"accessTime"`}funcHttpResponseSuccess(whttp.ResponseWriter,r*http.Request,datainterface{}){setResponse:=SetResponse{Status:http.StatusText(200),AccessTime:time.Now().Format("02-01-200615:04:05"),Data:data}response,_:=json.Marshal(setResponse)w.Header().Set("Content-Type","Application/json")w.WriteHeader(200)w.Write(response)}funcHttpResponseError(whttp.ResponseWriter,r*http.Request,datainterface{},codeint){setResponse:=SetResponse{Status:http.StatusText(code),AccessTime:time.Now().Format("02-01-200615:04:05"),Data:data}response,_:=json.Marshal(setResponse)w.Header().Set("Content-Type","Application/json")w.WriteHeader(code)w.Write(response)}
routes.go
packagebookimport("github.com/go-chi/chi""github.com/go-chi/chi/middleware""github.com/graphql-go/handler")funcRegisterRoutes(r*chi.Mux)*chi.Mux{/*GraphQL*/graphQL:=handler.New(&handler.Config{Schema:&Schema,Pretty:true,GraphiQL:true,})r.Use(middleware.Logger)r.Handle("/query",graphQL)returnr}
最后 , 创建名为main.go的文件 。
main.go
packagemainimport("github.com/go-chi/chi""graphql/book""graphql/infrastructure""log""net/http""net/url")funcmain(){routes:=chi.NewRouter()r:=book.RegisterRoutes(routes)log.Println("Serverreadyat8080")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有很多优点 , 但事实证明 , 与RESTAPI相比 , GraphQL处理文件上传和简单API的性能表现有所不足 。 因此 , 我们必须首先了解要构建的系统 , 是否适合将GraphQL用作应用程序的设计架构 。

文章图片
留言点赞关注
我们一起分享AI学习与发展的干货
如转载 , 请后台留言 , 遵守转载规范
推荐阅读
- 小龙虾|三农探析:池塘养殖小龙虾如何高产?高产养殖技术全解析
- 显微镜|假如人类可以把显微镜提升到40亿倍,是不是全新的宇宙观?
- 春天,吃鱼吃肉不如吃它,8块钱炒一盘,鲜香营养,好吃还不发胖
- 菜籽饼|菜籽饼被誉为果园之宝,但用错了烧苗烧根,果农如何来使用?
- 水产养殖|生态水产养殖如何提高鱼病的预防工作?大疆渔业这样建议
- 香芋跟南瓜还能如此吃,不做煎饼,软糯营养,好吃不上火,太美味
- 冬天,吃白菜吃菠菜不如它,营养极高,视力好了,肝脏也健康了
- 转基因|转基因背锅?去年已有不少弃种,如今产地低至3毛钱,果农愁销路
- 粉丝如此做真解馋,不蒸不炖不焖,好吃营养,老公一下子吃了一盘
- 早餐不吃馒头,教你如此做花卷,不用时间发面,松软香甜,真好吃
