GraphQL了解一下

GraphQL背景

Go語言的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  {
        articles():[Article!]!
        article(id: Int!): Article!
      }
      
    • 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、mut tion、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調試工具

graphiql.png

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

參考資料

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容