热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Go语言的GraphQL实践总结

Go语言的GraphQL实践总结,Go语言社区,Golang程序员人脉社

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,可能很多理解有误,欢迎指出。

参考资料

  • GraphQL官网中文版
  • 30分钟理解GraphQL核心概念
  • GitHub为什么开放一套GraphQL版本的API?
  • GraphQL入门
  • 在GraphQL中建模一个博客索引

推荐阅读
  • 本文深入探讨了分布式文件系统的核心概念及其在现代数据存储解决方案中的应用,特别是针对大规模数据处理的需求。文章不仅介绍了多种流行的分布式文件系统和NoSQL数据库,还提供了选择合适系统的指导原则。 ... [详细]
  • Navicat Premium中MySQL用户管理:创建新用户及高级设置
    本文作为Navicat Premium用户管理系列的第二部分,主要介绍如何创建新的MySQL用户,包括设置基本账户信息、密码策略、账户限制以及SSL配置等。 ... [详细]
  • [编程题] LeetCode上的Dynamic Programming(动态规划)类型的题目
    继上次把backTracking的题目做了一下之后:backTracking,我把LeetCode的动态规划的题目又做了一下,还有几道比较难的Medium的题和Hard的题没做出来,后面会继续 ... [详细]
  • Web3隐私协议Manta Network与区块链互操作性平台Axelar达成战略合作,共同推进跨链资产的隐私保护。 ... [详细]
  • Android开发经验分享:优化用户体验的关键因素
    随着Android市场的不断扩展,用户对于移动应用的期望也在不断提高。本文探讨了在Android开发中如何优化用户体验,以及为何用户体验的重要性超过了技术本身。 ... [详细]
  • C#爬虫Fiddler插件开发自动生成代码
    哈喽^_^一般我们在编写网页爬虫的时候经常会使用到Fiddler这个工具来分析http包,而且通常并不是分析一个包就够了的,所以为了把更多的时间放在分析http包上,自动化生成 ... [详细]
  • 在现代移动应用开发中,尤其是iOS应用,处理来自服务器的JSON数据是一项基本技能。无论是使用Swift还是PHP,有效地解析和利用JSON数据对于提升用户体验至关重要。本文将探讨如何在Swift中优雅地处理JSON,以及PHP中处理JSON的一些技巧。 ... [详细]
  • J2EE平台集成了多种服务、API和协议,旨在支持基于Web的多层应用开发。本文将详细介绍J2EE平台中的13项关键技术规范,涵盖从数据库连接到事务处理等多个方面。 ... [详细]
  • 设计模式系列-原型模式
    一、上篇回顾上篇创建者模式中,我们主要讲述了创建者的几类实现方案,和创建者模式的应用的场景和特点,创建者模式适合创建复杂的对象,并且这些对象的每个组成部分的详细创建步骤可以是动态的变化的,但 ... [详细]
  • MVC框架下使用DataGrid实现时间筛选与枚举填充
    本文介绍如何在ASP.NET MVC项目中利用DataGrid组件增强搜索功能,具体包括使用jQuery UI的DatePicker插件添加时间筛选条件,并通过枚举数据填充下拉列表。 ... [详细]
  • 本文档提供了详细的MySQL安装步骤,包括解压安装文件、选择安装类型、配置MySQL服务以及设置管理员密码等关键环节,帮助用户顺利完成MySQL的安装。 ... [详细]
  • Golang与微服务架构:构建高效微服务
    本文探讨了Golang在微服务架构中的应用,包括Golang的基本概念、微服务开发的优势、常用开发工具以及具体实践案例。 ... [详细]
  • 配置PicGo与Gitee结合Typora打造高效写作环境
    本文详细介绍了如何通过PicGo和Gitee搭建个人图床,并结合Typora实现高效的文章撰写。包括创建图床项目、生成访问令牌、安装配置PicGo和Typora等步骤。 ... [详细]
  • 基于OpenCV的小型图像检索系统开发指南
    本文详细介绍了如何利用OpenCV构建一个高效的小型图像检索系统,涵盖从图像特征提取、视觉词汇表构建到图像数据库创建及在线检索的全过程。 ... [详细]
  • 惠普战86 Pro G2:新一代商用台式机的性能与设计解析
    惠普战86 Pro G2台式机以其卓越的性能和紧凑的设计,满足了现代商务环境的需求。本文将详细介绍这款商用台式机的各项特点,包括其强大的硬件配置、精美的外观设计以及出色的稳定性和安全性。 ... [详细]
author-avatar
zjymeimei706
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有