Facebook:如何在Golang中搭建GraphQL?
本文轉載自公眾號“讀芯術”(ID:AI_Discovery)。
多年來,人們一直在使用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 */
- 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用作應用程序的設計架構。