1. 程式人生 > >Go語言的GraphQL實踐總結

Go語言的GraphQL實踐總結

GraphQL背景

REST API的使用方式是,server定義一系列的介面,client呼叫自己需要的介面,獲取目標資料進行整合。REST API開發中遇到的問題:

  • 擴充套件性 ,隨著API的不斷髮展,REST API的介面會變得越來臃腫。
  • 無法按需獲取 ,一個返回id, name, age, city, addr, email的介面,如果僅獲取部分資訊,如name, age,卻必須返回介面的全部資訊,然後從中提取自己需要的。壞處不僅會增加網路傳輸量,並且不便於client處理資料
  • 一個請求無法獲取所需全部資源 ,例如client需要顯示一篇文章的內容,同時要顯示評論,作者資訊,那麼就需要呼叫文章、評論、使用者的介面。壞處造成服務的的維護困難,以及響應時間變長

    • 原因: REST API通常由多個端點組成,每個端點代表一種資源。所以,當client需要多個資源是,它需要向REST API發起多個請求,才能獲取到所需要的資料。
  • REST API不好處理的問題 , 比如確保client提供的引數是型別安全的,如何從程式碼生成API的文件等。

GraphQL解決的問題:

  • 請求你的資料不多不少 :GraphQL查詢總是能準確獲得你想要的資料,不多不少,所以返回的結果是可預測的。
  • 獲取多個資源只用一個請求 :GraphQL查詢不僅能夠獲得資源的屬性,還能沿著資源間進一步查詢,所以GraphQL可以通過一次請求就獲取你應用所需的所有資料。
  • 描述所有的可能型別系統
    : GraphQL API基於型別和欄位的方式進行組成,使用型別來保證應用只請求可能的型別,同時提供了清晰的輔助性錯誤資訊。
  • 使用你現有的資料和程式碼: GraphQL讓你的整個應用共享一套API,通過GraphQL API能夠更好的利用你的現有資料和程式碼。GraphQL 引擎已經有多種語言實現,GraphQL不限於某一特定資料庫,可以使用已經存在的資料、程式碼、甚至可以連線第三方的APIs。
  • API 演進無需劃分版本: 給GraphQL API新增欄位和型別而無需影響現有查詢。老舊欄位可以廢棄,從工具中隱藏。

什麼是GraphQL

GraphQL官網給出定義:GraphQL既是一種用於API的查詢語言

也是一個滿足你資料查詢的執行時 。GraphQL對你的API中的資料提供了一套易於理解的完整描述 ,使得客戶端能夠準確地獲得它需要的資料 ,而且沒有任何冗餘,也讓API更容易地隨著時間推移而演進,還能用於構建強大的開發者工具。

  • API不是用來呼叫的嗎?是的,者正是GraphQL的強大之處,引用官方文件的一句話ask exactly what you want
  • 本質上來說GraphQL是一種查詢語言
  • 上述的定義其實很難理解,只有真的使用過GraphQL才能夠理解。

在GraphQL中,通過定義一張Schema和宣告一些Type來達到上述描述的功能,需要學習:

  • 對於資料模型的抽象是通過Type來描述的 ,如何定義Type?
  • 對於介面獲取資料的邏輯是通過schema來描述的 ,如何定義schema?

如何定義Type

對於資料模型的抽象是通過Type來描述的,每一個Type有若干Field組成,每個Field又分別指向某個Type。

GraphQL的Type簡單可以分為兩種,一種是scalar type(標量型別) ,另一種是object type(物件型別)。

scalar type

GraphQL中的內建的標量包含,String、Int、Float、Boolean、Enum,除此之外,GraphQL中可以通過scalar宣告一個新的標量 ,比如:

  • prisma ——一個使用GraphQL來抽象資料庫操作的庫中,還有DataTime(日期格式)和主鍵(ID)。
  • 在使用GraphQL實現檔案上傳介面時,需要宣告一個Upload標量來代表要上傳的檔案。
  • 標量是GraphQL型別系統中最小的顆粒。

object type

僅有標量是不夠抽象一些複雜的資料模型,這時需要使用物件型別。通過物件型別來構建GraphQL中關於一個數據模型的形狀,同時還可以宣告各個模型之間的內在關聯(一對多,一對一或多對多)。

一對一模型

type Article {
  id: ID
  text: String
  isPublished: Boolean
  author: User
}

上述程式碼,聲明瞭一個Article型別,它有3個Field,分別是id(ID型別)、text(String型別)、isPublished(Boolean型別)以及author(新建的物件型別User),User型別的宣告如下:

type User {
  id: ID
  name: String
}

lType Modifier

型別修飾符,當前的型別修飾符有兩種,分別是List和Required ,語法分別為[Type]和[Type!],兩者可以組合:

  • [Type]! :列表本身為必填項,但內部元素可以為空
  • [Type!] :列表本身可以為空,但是其內部元素為必填
  • [Type!]! :列表本身和內部元素均為必填

如何定義Schema

schema用來描述對於介面獲取資料邏輯 ,GraphQL中使用Query來抽象資料的查詢邏輯,分為三種,分別是query(查詢)、mutation(更改)、subscription(訂閱) 。API的介面概括起來有CRUD(建立、獲取、更改、刪除)四類,query可以覆蓋R(獲取)的功能,mutation可以覆蓋(CUD建立、更改、刪除)的功能。

注意: Query特指GraphQL中的查詢(包含三種類型),query指GraphQL中的查詢型別(僅指查詢型別)。

Query

  • query(查詢):當獲取資料時,選擇query型別
  • mutation(更改): 當嘗試修改資料時,選擇mutation型別
  • subscription(訂閱):當希望資料更改時,可以進行訊息推送,使用subscription型別(針對當前的日趨流行的real-time應用提出的)。

以Article為資料模型,分別以REST和GraphQL的角度,編寫CURD的介面

  • Rest介面

    • GET /api/v1/articles/
    • GET /api/v1/article/:id/
    • POST /api/v1/article/
    • DELETE /api/v1/article/:id/
    • PATCH /api/v1/article/:id/
  • GraphQL Query

    • query型別
      query {
      articles():[Article!]!
      article(id: Int!): Article!
      }
    • mutation型別
      mutation {
      createArticle(): Article!
      updateArticle(id: Int): Article!
      deleteArticle(id: Int): Article!
      }

    注意

    • GraphQL是按照型別來劃分職能的query、mutation、ssubscription,同時必須明確宣告返回的資料型別。

    • 如果實際應用中對於評論列表有real-time 的需求,該如何處理?

    • 在REST中,可以通過長連線,或者通過提供一些帶驗證的獲取長連線URL的介面,比如POST /api/v1/messages/之後長連線會將新的資料進行實時推送。

    • 在GraphQL中,會以更加宣告式的方式進行宣告,如下:

      subscription {
      updatedArticle() {
        mutation
        node {
          comments: [Comment!]!
        }
      }
      }

      此處聲明瞭一個subscription,這個subscription會在有新的Article被建立或者更新時,推送新的資料物件。實際上內部仍然是建立於長連線之上

    Resolve

    上述的描述並未說明如何返回相關操作(query、mutation、subscription)的資料邏輯。所有此處引入一個更核心的概念Resolve(解析函式)

    GraphQL中,預設有這樣的約定,Query(包括query、mutation、subscription)和與之對應的Resolve是同名的,比如關於articles(): [Articles!]!這個query,它的Resolve的名字必然叫做articles

    以已經宣告的articles的query為例,解釋下GraphQL的內部工作機制

    Query {
    articles {
         id
         author {
            name
         }
         comments {
        id
        desc
        author
      }
    }
    }

    按照如下步驟進行解析:

    • 首先進行第一次解析,當前的型別是query 型別,同時Resolver的名字為articles
    • 之後會嘗試使用articles的Resolver獲取解析資料,第一層解析完畢
    • 之後對第一層解析的返回值,進行第二層解析,當前articles包含三個子query ,分別是id、author和comments
    • id在Author型別中為標量型別,解析結束
    • author在articles型別中為物件型別User,嘗試使用User的Resolver獲取資料,當前field解析完畢。
    • 之後對第二層解析的返回值,進行第三層解析,當前author還包含一個query,name是標量型別,解析結束
    • comments解析同上

概括總結GraphQL大體解析流程就是遇見一個Query之後,嘗試使用它的Resolver取值,之後再對返回值進行解析,這個過程是遞迴的,直到所有解析Field型別是Scalar Type(標量型別)為止。整個解析過程可以想象為一個很長的Resolver Chain(解析鏈)。

Resolver本身的宣告在各個語言中是不同的,它代表資料獲取的具體邏輯。它的函式簽名(以golang為例):

func(p graphql.ResolveParams) (interface{}, error) {}

// ResolveParams Params for FieldResolveFn()
type ResolveParams struct {
    // Source is the source value
    Source interface{}

    // Args is a map of arguments for current GraphQL request
    Args map[string]interface{}

    // Info is a collection of information about the current execution state.
    Info ResolveInfo

    // Context argument is a context value that is provided to every resolve function within an execution.
    // It is commonly
    // used to represent an authenticated user, or request-specific caches.
    Context context.Context
}

值得注意的是,Resolver內部實現對於GraphQL完全是黑盒狀態。這意味著Resolver如何返回資料、返回什麼樣的資料、從哪裡返回資料,完全取決於Resolver本身。GraphQL在實際使用中常常作為中間層來使用,**資料的獲取通過Resolver來封裝,內部資料獲取的實現可能基於RPC、REST、WS、SQL等多種不同的方式。

GraphQL例子

下面這部分將會展示一個用graphql-go實現的使用者管理的例子,包括獲取全部使用者資訊、獲取指定使用者資訊、修改使用者名稱稱、刪除使用者的功能,以及如何建立列舉型別的功能,完整程式碼在這裡

生成後的schema檔案內容如下:

type Mutation {
  """[使用者管理] 修改使用者名稱稱"""
  changeUserName(
    """使用者ID"""
    userId: Int!

    """使用者名稱稱"""
    userName: String!
  ): Boolean

  """[使用者管理] 建立使用者"""
  createUser(
    """使用者名稱稱"""
    userName: String!

    """使用者郵箱"""
    email: String!

    """使用者密碼"""
    pwd: String!

    """使用者聯絡方式"""
    phone: Int
  ): Boolean

  """[使用者管理] 刪除使用者"""
  deleteUser(
    """使用者ID"""
    userId: Int!
  ): Boolean
}

type Query {
  """[使用者管理] 獲取指定使用者的資訊"""
  UserInfo(
    """使用者ID"""
    userId: Int!
  ): userInfo

  """[使用者管理] 獲取全部使用者的資訊"""
  UserListInfo: [userInfo]!
}

"""使用者資訊描述"""
type userInfo {
  """使用者email"""
  email: String

  """使用者名稱稱"""
  name: String

  """使用者手機號"""
  phone: Int

  """使用者密碼"""
  pwd: String

  """使用者狀態"""
  status: UserStatusEnum

  """使用者ID"""
  userID: Int
}

"""使用者狀態資訊"""
enum UserStatusEnum {
  """使用者可用"""
  EnableUser

  """使用者不可用"""
  DisableUser
}

注意

  • GraphQL基於golang實現的例子比較少
  • GraphQL的schema可以自動生成,具體操作可檢視graphq-cli文件,步驟大致包括npm包的安裝、graphql-cli工具的安裝,配置檔案的更改(此處需要指定服務對外暴露的地址) ,執行graphql get-schema 命令。

GraphQL API以及Rsolve函式定義


type UserInfo struct {
    UserID uint64               `json:"userID"`
    Name   string               `json:"name"`
    Email  string               `json:"email"`
    Phone  int64                `json:"phone"`
    Pwd    string               `json:"pwd"`
    Status model.UserStatusType `json:"status"`
}
//這段內容是如何使用GraphQL定義列舉型別
var UserStatusEnumType = graphql.NewEnum(graphql.EnumConfig{
    Name:        "UserStatusEnum",
    Description: "使用者狀態資訊",
    Values: graphql.EnumValueConfigMap{
        "EnableUser": &graphql.EnumValueConfig{
            Value:       model.EnableStatus,
            Description: "使用者可用",
        },
        "DisableUser": &graphql.EnumValueConfig{
            Value:       model.DisableStatus,
            Description: "使用者不可用",
        },
    },
})

var UserInfoType = graphql.NewObject(graphql.ObjectConfig{
    Name:        "userInfo",
    Description: "使用者資訊描述",
    Fields: graphql.Fields{
        "userID": &graphql.Field{
            Description: "使用者ID",
            Type:        graphql.Int,
        },
        "name": &graphql.Field{
            Description: "使用者名稱稱",
            Type:        graphql.String,
        },
        "email": &graphql.Field{
            Description: "使用者email",
            Type:        graphql.String,
        },
        "phone": &graphql.Field{
            Description: "使用者手機號",
            Type:        graphql.Int,
        },
        "pwd": &graphql.Field{
            Description: "使用者密碼",
            Type:        graphql.String,
        },
        "status": &graphql.Field{
            Description: "使用者狀態",
            Type:        UserStatusEnumType,
        },
    },
})

query與mutation的定義

var MutationType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Mutation",
    Fields: graphql.Fields{
        "createUser": &graphql.Field{
            Type:        graphql.Boolean,
            Description: "[使用者管理] 建立使用者",
            Args: graphql.FieldConfigArgument{
                "userName": &graphql.ArgumentConfig{
                    Description: "使用者名稱稱",
                    Type:        graphql.NewNonNull(graphql.String),
                },
                "email": &graphql.ArgumentConfig{
                    Description: "使用者郵箱",
                    Type:        graphql.NewNonNull(graphql.String),
                },
                "pwd": &graphql.ArgumentConfig{
                    Description: "使用者密碼",
                    Type:        graphql.NewNonNull(graphql.String),
                },
                "phone": &graphql.ArgumentConfig{
                    Description: "使用者聯絡方式",
                    Type:        graphql.Int,
                },
            },
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                userId, _ := strconv.Atoi(GenerateID())
                user := &model.User{
                  //展示如何解析傳入的引數
                    Name: p.Args["userName"].(string),
                    Email: sql.NullString{
                        String: p.Args["email"].(string),
                        Valid:  true,
                    },
                    Pwd:    p.Args["pwd"].(string),
                    Phone:  int64(p.Args["phone"].(int)),
                    UserID: uint64(userId),
                    Status: int64(model.EnableStatus),
                }
                if err := model.InsertUser(user); err != nil {
                    log.WithError(err).Error("[mutaition.createUser] invoke InserUser() failed")
                    return false, err
                }
                return true, nil

            },
        },

    },
})

var QueryType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Query",
    Fields: graphql.Fields{
        "UserListInfo": &graphql.Field{
            Description: "[使用者管理] 獲取指定使用者的資訊",
          //定義了非空的list型別
            Type:        graphql.NewNonNull(graphql.NewList(UserInfoType)),
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                users, err := model.GetUsers()
                if err != nil {
                    log.WithError(err).Error("[query.UserInfo] invoke InserUser() failed")
                    return false, err
                }
                usersList := make([]*UserInfo, 0)
                for _, v := range users {
                    userInfo := new(UserInfo)
                    userInfo.Name = v.Name
                    userInfo.Email = v.Email.String
                    userInfo.Phone = v.Phone
                    userInfo.Pwd = v.Pwd
                    userInfo.Status = model.UserStatusType(v.Status)
                    usersList = append(usersList, userInfo)

                }
                return usersList, nil

            },
        },
    },
})

注意

  • 此處僅展示了部分例子
  • 此處筆者僅列舉了query、mutation型別的定義

如何定義服務main函式

type ServerCfg struct {
    Addr      string
    MysqlAddr string
}

func main() {
    //load config info
    m := multiconfig.NewWithPath("config.toml")
    svrCfg := new(ServerCfg)
    m.MustLoad(svrCfg)
    //new graphql schema
    schema, err := graphql.NewSchema(
        graphql.SchemaConfig{
            Query:    object.QueryType,
            Mutation: object.MutationType,
        },
    )
    if err != nil {
        log.WithError(err).Error("[main] invoke graphql.NewSchema() failed")
        return
    }

    model.InitSqlxClient(svrCfg.MysqlAddr)
    h := handler.New(&handler.Config{
        Schema:   &schema,
        Pretty:   true,
        GraphiQL: true,
    })
    http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
        ctx := context.Background()
        //read user_id from gateway
        userIDStr := r.Header.Get("user_id")
        if len(userIDStr) > 0 {
            userID, err := strconv.Atoi(userIDStr)
            if err != nil {
                w.WriteHeader(http.StatusBadRequest)
                w.Write([]byte(err.Error()))
                return
            }
            ctx = context.WithValue(ctx, "ContextUserIDKey", userID)
        }
        h.ContextHandler(ctx, w, r)

    })
    log.Fatal(http.ListenAndServe(svrCfg.Addr, nil))
}

展示下GraphQL自帶的GraphiQL除錯工具

這裡寫圖片描述

筆者初次接觸GraphQL,可能很多理解有誤,歡迎指出。

參考資料