编程技术分享

  • 首页
  1. 首页
  2. Go
  3. 正文

GraphQL介绍及使用

2023年10月7日 3354点热度 1人点赞 0条评论

1、前言

在目前比较流行的微服务架构中,一般为了将存储和应用解耦,会将存储作为单独的服务提供给应用使用,由存储服务对接底层各种存储引擎,如MySQL、MongoDB、PostgreSQL等。这样可以统一各种存储引擎的差异,使应用对存储引擎无感知。要消除这种差异,需要制定一个统一的接口语言,应用使用该接口语言来操作数据。GraphQL是Facebook开源的一种接口语言,它可以提供灵活、高效的数据操作方式,能够精确的获取指定数据。本文接下来介绍GraphQL及用于构建其Go服务的gqlgen库的使用,最后会涉及到针对gqlgen的改造,以方便管理大型项目。其中gqlgen的改造代码参考:https://github.com/jemuelmiao/gqlgen。

2、基本用法

(1)创建GraphQL项目

> mkdir example && cd example
> go mod init example
> go get github.com/99designs/gqlgen
> go run github.com/99designs/gqlgen init
> go mod tidy

创建后项目目录如下:

(2)调整请求处理逻辑:schema.resolvers.go

(3)启动服务:go run server.go

(4)请求数据

浏览器访问http://localhost:8080展示结果如下:

同样也可以通过调用API(http://localhost:8080/query)请求数据:

(5)修改结构定义

修改schema定义文件后,需执行go run github.com/99designs/gqlgen generate重新生成代码。

3、GraphQL类型

3.1 内置类型

GraphQL中类型分为几种:基本类型、扩展类型、自定义类型。其中基本类型包括:Float、String、Boolean、Int、ID,扩展类型包括:Time、Map、Upload、Any、Uint。这些类型和Go类型的映射关系如下:

在schema文件中使用类型有三个步骤:实现Go类型序列化方法、在gqlgen.yml中映射GraphQL scalar到Go类型、在schema中增加GraphQL scalar。其中基本类型在schema中可以直接使用,无需经过这三个步骤。扩展类型(除Uint外)已经在gqlgen中实现序列化和到Go类型的映射,使用时只需在schema中增加GraphQL scalar即可。Uint在gqlgen中实现了序列化,但未映射到Go类型,使用时需要手动映射到Go类型以及在schema中增加GraphQL scalar。gqlgen源码中各类型实现在目录gqlgen/graphql中,内置类型映射在gqlgen/codegen/config/config.go的injectBuiltins函数中。

3.2 自定义类型

当GraphQL提供的类型无法满足需要时,可以通过自定义类型提供。自定义类型有两种:用户定义类型、三方库类型。两者的区别在于用户是否能够修改类型,为其增加方法。下面介绍如何扩展类型。

3.2.1 用户定义类型

(1)实现Go类型

在github上建立一个仓库用于管理自定义类型,实现graphql.Marshaler和graphql.Unmarshaler两个接口或graphql.ContextMarshaler和graphql.ContextUnmarshaler两个接口,两者的主要区别是后者在类型序列化过程中需要使用请求的context。示例仓库如https://github.com/jemuelmiao/gqlgen_type,自定义类型Long、LongContext实现分别在文件type_long.go、type_long_context中。

graphql.Marshaler和graphql.Unmarkshaler接口定义如下:

type Marshaler interface {
    MarshalGQL(w io.Writer)
}
type Unmarshaler interface {
    UnmarshalGQL(v interface{}) error
}

graphql.ContextMarshaler和graphql.ContextUnmarshaler接口定义如下:

type ContextMarshaler interface {
    MarshalGQLContext(ctx context.Context, w io.Writer) error
}
type ContextUnmarshaler interface {
    UnmarshalGQLContext(ctx context.Context, v interface{}) error
}

自定义Long类型实现如下:

package gqlgen_type

import (
    "fmt"
    "io"
)

type Long int64

func (l Long) MarshalGQL(w io.Writer) {
    w.Write([]byte(fmt.Sprintf("%v", l)))
}

func (l *Long) UnmarshalGQL(v interface{}) error {
    value, ok := v.(int64)
    if !ok {
        return fmt.Errorf("Long must be int64")
    }
    *l = Long(value)
    return nil
}

自定义LongContext类型实现如下:

package gqlgen_type

import (
    "context"
    "fmt"
    "io"
)

type LongContext int64

func (l LongContext) MarshalGQLContext(ctx context.Context, w io.Writer) error {
    w.Write([]byte(fmt.Sprintf("%v", l)))
    return nil
}

func (l *LongContext) UnmarshalGQLContext(ctx context.Context, v interface{}) error {
    value, ok := v.(int64)
    if !ok {
        return fmt.Errorf("LongContext must be int64")
    }
    *l = LongContext(value)
    return nil
}

(2)映射GraphQL scalar到Go类型

在gqlgen.yaml中增加:

models:
  Long:
    model: github.com/jemuelmiao/gqlgen_type.Long

(3)增加GraphQL scalar

在schema文件中增加:scalar Long

(4)使用scalar类型

在schema文件中定义字段类型:

type UserType {
    field1: Long
}

3.2.2 三方库类型

该类型和用户定义类型除了实现序列化方式不一样外,其他都一样,例如类型LongThirdParty实现方式为:

package gqlgen_type

import (
    "fmt"
    "github.com/99designs/gqlgen/graphql"
    "io"
)

func MarshalLongThirdParty(v int64) graphql.Marshaler {
    return graphql.WriterFunc(func(w io.Writer) {
        w.Write([]byte(fmt.Sprintf("%v", v)))
    })
}

func UnmarshalLongThirdParty(v interface{}) (int64, error) {
    value, ok := v.(int64)
    if !ok {
        return 0, fmt.Errorf("LongThirdParty must be int64")
    }
    return value, nil
}

4、GraphQL指令

GraphQL指令类似装饰器,主要用于实现一些通用的功能,如字段tag、有效性检查、API权限检查等,GraphQL提供了几个内置指令,另外也可以自己扩展指令。

4.1 内置指令

内置指令包括:goModel、goField、goTag。其中goModel用于将Go结构映射为一个GraphQL模型,goField用于将Go结构字段映射为GraphQL模型字段,goTag用于自定义GraphQL模型字段的标签。

使用内置指令前需在schema文件中定义,定义如下:

directive @goModel(
    model: String
    models: [String!]
) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION

directive @goField(
    forceResolver: Boolean
    name: String
    omittable: Boolean
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION

directive @goTag(
    key: String!
    value: String
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION

使用方式如下:

type User @goModel(model: "github.com/my/app/models.User") {
    id: ID! @goField(name: "todoId")
    name: String!
          @goField(forceResolver: true)
          @goTag(key: "xorm", value: "-")
          @goTag(key: "yaml")
}

4.2 自定义指令

GraphQL除了内置指令外,还支持自定义指令。接下来通过字段校验来介绍如何创建自定义指令。

(1)在schema文件中定义指令

directive @validate(constraint: String!) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION

(2)在待校验字段上加入校验规则

input CreateProjectReq {
    name: String! @validate(constraint: "required,max=20")
    desc: String @validate(constraint: "max=100")
    task_count: Int64 @validate(constraint: "gte=0")
    segment_count: Int64 @validate(constraint: "gte=0")
    stage_ids: [Int32]!
}

(3)重新生成代码

go run github.com/99designs/gqlgen generate

(4)增加指令处理逻辑

...

conf := graph.Config{
    Resolvers: &resolver.Resolver{},
}
conf.Directives.Validate = func(ctx context.Context, obj interface{}, next graphql.Resolver, constraint string) (res interface{}, err error) {
    val, err := next(ctx)
    if err != nil {
        panic(err)
    }
    fieldName := *graphql.GetPathContext(ctx).Field
    err = validate.Var(val, constraint)
    if err != nil {
        validationErrors := err.(validator.ValidationErrors)
        return nil, fmt.Errorf("%v:%v", fieldName, validationErrors[0].Translate(trans))
    }
    return val, nil
}
srv := handler.NewDefaultServer(graph.NewExecutableSchema(conf))
...

(5)重启GraphQL服务器

go run server.go

(6)验证

5、GraphQL插件

5.1 插件开发

gqlgen提供了接入插件方式调整代码生成规则,生成代码时,会优先初始化内置插件,然后初始化用户插件,具体可参考https://github.com/99designs/gqlgen/blob/master/api/generate.go。gqlgen使用的内置插件有modelgen、resolvergen、federation(执行顺序:modelgen=》resolvergen=》federation=》用户插件)。插件可以调整的内容有几种类型:MutateConfig、GenerateCode、InjectSourceEarly、InjectSourceLate、Implement(类型执行顺序:InjectSourceEarly=》InjectSourceLate=》MutateConfig=》GenerateCode=》Implement),具体可参考https://github.com/99designs/gqlgen/blob/master/plugin/plugin.go,开发插件时可以根据插件功能实现其中一种或多种类型,其中常见的是MutateConfig、GenerateCode,MutateConfig用于在生成代码前修改配置,GenerateCode用于修改生成的代码。

开发插件时根据功能实现https://github.com/99designs/gqlgen/blob/master/plugin/plugin.go中一个或多个函数,具体实现方式可以参考内置插件https://github.com/99designs/gqlgen/blob/master/plugin/resolvergen/resolver.go。插件开发完成后,接入GraphQL服务器方式如下:

func main() {
    ...
    cfg, err := config.LoadConfigFromDefaultLocations()
    if err != nil {
        os.Exit(1)
    }
    var options []api.Option
    options = append(options, api.AddPlugin(yourplugin.New()))
    err = api.Generate(cfg, options...)
    if err != nil {
        os.Exit(2)
    }
    ...
}

6、实战用法

6.1 二次改造

原生gqlgen生成的组织结构不满足大型项目需求,所有resolver文件全部在一个目录,对于单一项目来说没问题,但如果作为多个项目的统一接入层,显然这种文件组织方式会造成混乱,不利于维护管理。因此需要对gqlgen进行改造,改造需求:graphql文件按照项目子目录形式管理,生成的resolver文件按照相同的子目录形式组织,支持共用的resolver文件,graphql文件以及对应的resolver文件组织结构示例如下:

原始graphql文件由两个项目datamanager、datamark组成,里面包含各自项目的结构定义,另外顶层schema目录中包含了所有项目共用的结构定义文件comm.graphql。最终需要生成的resolver目录也要求按照各自项目进行组织,公共文件在顶层目录中。

整个改造以插件形式接入,原版gqlgen有个内嵌插件resolvergen用于生成resolver相关代码,其调用是在api/generate.go/Generate()中,现在需要将其替换为新插件resolvergen_v2。改造相关的代码见:https://github.com/jemuelmiao/gqlgen,可查看提交日志查看针对原版gqlgen做了哪些改动。

6.1.1 总入口resolver.go

resolver.go文件中包含了所有graphql中定义的Query、Mutation,这个文件在原版gqlgen中只包含了type Resolver struct{},改造后需要将各个项目中所有Query、Mutation进行定义,然后通过导入子模块的方式调用每个模块的具体实现。

为方便生成resolver.go,需为resolver.go创建一个新的代码模板,这里我命名为resolver_interface.gotpl,具体内容见:https://github.com/jemuelmiao/gqlgen/blob/main/plugin/resolvergen_v2/resolver_interface.gotpl。

在插件generatePerSchema函数中,将mutationResolver、queryResolver的定义加入生成的resolver.go中,并且针对各个项目中定义的Query、Mutation函数,通过import方式将对应函数引入并调用,为了防止子模块重名,采用alias方式引入子模块,另外在顶层目录的公用文件因为和resolver.go在同级目录,可以直接调用公用文件的函数,无需import,因此需保证公用部分不能有重名函数,这也是很合理的。具体实现见:https://github.com/jemuelmiao/gqlgen/blob/main/plugin/resolvergen_v2/resolver.go。生成的resolver.go示例如下:

6.1.2 自定义结构的resolver

自定义Query、Mutation函数包含了具体实现,这些函数在原版gqlgen中是通过mutationResolver、queryResolver的方法定义的,改造后为单独的函数。为方便生成每个自定义Query、Mutation函数,需为他们创建一个统一的代码模板,这里我命名为resolver_schema.gotpl,具体内容见:https://github.com/jemuelmiao/gqlgen/blob/main/plugin/resolvergen_v2/resolver_schema.gotpl。

在插件generatePerSchema函数中,生成每个Query、Mutation函数的定义及实现,首次生成时会使用默认实现,再次生成时会使用已有实现。具体实现见:https://github.com/jemuelmiao/gqlgen/blob/main/plugin/resolvergen_v2/resolver.go。生成的文件示例如下:

6.1.3 重写所有resolver

上面提到再次生成每个Query、Mutation的函数时,需要保证已有的实现代码不丢失,因此还需要改造resolver重写逻辑,原版gqlgen重写逻辑是针对单个package,需要将其调整为支持多个package,用于区分不同子模块,调整后结构如下:

type MultiRewriter struct {
    pkgs   map[string]*packages.Package // {dirpath: package}
    files  map[string]string
    copied map[ast.Decl]bool
}

pkgs通过子模块路径名进行区分。具体实现见:https://github.com/jemuelmiao/gqlgen/blob/main/internal/rewrite/multirewriter.go。

6.2 统一存储

在实际项目中,通常会用到各种各样的存储服务,不同存储服务千差万别,对接成本和切换成本较高,为降低应用层对存储服务的依赖,可以抽象出存储中间层,将各种存储服务管理起来,对外提供统一的接口,架构图如下:

在使用统一存储时,可以在生成的resolver函数中使用合适的存储引擎操作数据,使用MongoDB示例如下(实际使用时可以封装各种引擎,方便切换):

标签: 暂无
最后更新:2023年10月7日

jemuel

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >
文章目录
  • 1、前言
  • 2、基本用法
  • 3、GraphQL类型
    • 3.1 内置类型
    • 3.2 自定义类型
      • 3.2.1 用户定义类型
      • 3.2.2 三方库类型
  • 4、GraphQL指令
    • 4.1 内置指令
    • 4.2 自定义指令
  • 5、GraphQL插件
    • 5.1 插件开发
  • 6、实战用法
    • 6.1 二次改造
      • 6.1.1 总入口resolver.go
      • 6.1.2 自定义结构的resolver
      • 6.1.3 重写所有resolver
    • 6.2 统一存储
最新 热点 随机
最新 热点 随机
K8S源码分析系列2—远程调试K8S组件 Volcano源码分析系列—调度篇 K8S源码分析系列1—搭建K8S调试集群 K8S Controller开发 6.5840 Lab 1: MapReduce MongoDB源码分析系列1——编译环境搭建
K8S源码分析系列2—远程调试K8S组件
Volcano源码分析系列—调度篇 Java热更新 Golang优先级调度 MySQL源码分析系列5——ibd解析 Go channel源码分析 MongoDB源码分析系列1——编译环境搭建

COPYRIGHT © 2021 www.miaozhouguang.com. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS

粤ICP备2022006024号

粤公网安备 44030602006568号