gqlgen学习笔记

1,057 阅读6分钟

简介

gqlgen 是一个用于构建GraphQL服务器的Go语言库。

  • gqlgen基于Schema构建,可以通过Schema Definition Language定义API
  • gqlgen倾向于类型安全,所以不会出现map[string]interface
  • gqlgen支持代码生成,只需要关注业务相关代码即可

快速开始

  1. 初始化一个module

    mkdir demo
    cd demo
    go mod init demo
    
  2. 引用gqlgen

    1. 创建tool.go文件

    2. 写入一下内容

      //go:build tools
      // +build toolspackage tools
      ​
      import (
          _ "github.com/99designs/gqlgen"
      )
      ​
      
    3. 执行go mod tidy

  3. 初始化gqlgen配置和模型

    go run github.com/99designs/gqlgen init
    

    gqlgen为构建好GraphQL服务端代码

  4. 启动GraphQL服务端代码

    go run server.go
    
  5. 一些其他资料

教程

  1. 构建项目

    1. 初始化go module
    mkdir gqlgen-todos
    cd gqlgen-todos
    go mod init github.com/[username]/gqlgen-todos
    
    1. 在当前目录创建一个tools.go文件,并添将gqlgen作为匿名以来添加
    //go:build tools
    // +build toolspackage tools
    ​
    import (
        _ "github.com/99designs/gqlgen"
    )
    
    1. 使用go mod tidy命令将依赖自动添加到go.mod中
    2. 根据需求,更新gqlgen版本
    go get -d github.com/99designs/gqlgen<@VERSION>
    

    如果只是看看效果,可以暂时不管

  2. 构建服务

    1. 生成服务框架
    go run github.com/99designs/gqlgen init
    

    如果已经安装了gqlgen工具,可以直接使用gqlgen init命令(前提是已经将GOPATH/bin加入环境变量PATH中)安装命令如下:

    go install github.com/99designs/gqlgen
    

    gqlgen会生成默认的工程框架,结构如下

    ├── go.mod
    ├── go.sum
    ├── gqlgen.yml               - The gqlgen config file, knobs for controlling the generated code.
    ├── graph
    │   ├── generated            - A package that only contains the generated runtime
    │   │   └── generated.go
    │   ├── model                - A package for all your graph models, generated or otherwise
    │   │   └── models_gen.go
    │   ├── resolver.go          - The root graph resolver type. This file wont get regenerated
    │   ├── schema.graphqls      - Some schema. You can split the schema into as many graphql files as you like
    │   └── schema.resolvers.go  - the resolver implementation for schema.graphql
    └── server.go                - The entry point to your app. Customize it however you see fit
    
    1. 定义的模式

      schema.graphqls为模式定义文件,实际应用中需要有用户定义。

      默认生成的schema.graphqls文件内容如下:

      type Todo {
        id: ID!
        text: String!
        done: Boolean!
        user: User!
      }
      ​
      type User {
        id: ID!
        name: String!
      }
      ​
      type Query {
        todos: [Todo!]!
      }
      ​
      input NewTodo {
        text: String!
        userId: String!
      }
      ​
      type Mutation {
        createTodo(input: NewTodo!): Todo!
      }
      

      GraphQL语法在后文中介绍,此处内容着重关注Query和Mutation两个type即可。对照graph/schema.resolvers.go中内容阅读,可以看到,其中的方法正好与schema中对应。

      func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
          panic(fmt.Errorf("not implemented"))
      }
      ​
      func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
          panic(fmt.Errorf("not implemented"))
      }
      
    2. 依赖注入

      在具体实现前,我们现在graph/resolver.go文件中为Resolver添加一个字段。

      type Resolver struct{
          todos []*model.Todo
      }
      

      graph/resolver.go文件是以来注入文件,仅在项目init自动生成一次,后续的generate不会发生修改。需要注入的以来由用户自定义。此时注入一个todo的切片,用作内存数据暂存。

    3. 实现方法

      此时我们返回graph/schema.resolvers.go实现之前两个方法。

      func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
          todo := &model.Todo{
              Text:   input.Text,
              ID:     fmt.Sprintf("T%d", rand.Int()),
              User: &model.User{ID: input.UserID, Name: "user " + input.UserID},
          }
          r.todos = append(r.todos, todo)
          return todo, nil
      }
      ​
      func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
          return r.todos, nil
      }
      
    4. 启动服务

      go run server.go
      

      此时可以通过访问localhost:8080访问页面。页面首次加载时间比可能较长,请耐心等待。

    5. 测试

      根据页面提示分别执行创建和查询操作。

      mutation createTodo {
        createTodo(input: { text: "todo", userId: "1" }) {
          user {
            id
          }
          text
          done
        }
      }
      
      query findTodos {
        todos {
          text
          done
          user {
            name
          }
        }
      }
      
    6. 自定义模型

      1. 修改配置文件gqlgen.yml,取消对自动绑定内容的注释。
      # gqlgen will search for any type names in the schema in these go packages
      # if they match it will use them, otherwise it will generate them.
      autobind:
       - "github.com/[username]/gqlgen-todos/graph/model"
      

      自动绑定的功能是,从内容中匹配对应shecma中的名称,如果存在则不会由gqlgen生成。

      1. 创建自定义模型

        创建新文件graph/model/todo.go,内容如下:

        package model
        ​
        type Todo struct {
            ID     string `json:"id"`
            Text   string `json:"text"`
            Done   bool   `json:"done"`
            UserID   string  `json:"user"`
        }
        
      2. 生成代码

        go run github.com/99designs/gqlgen generate
        

        我们会发现``graph/schema.resolvers.go` 中出现了一个新的方法。

        func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
            todo := &model.Todo{
                Text:   input.Text,
                ID:     fmt.Sprintf("T%d", rand.Int()),
                User:   &model.User{ID: input.UserID, Name: "user " + input.UserID},
            }
            r.todos = append(r.todos, todo)
            return todo, nil
        }
        ​
        func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
            return &model.User{ID: obj.UserID, Name: "user " + obj.UserID}, nil
        }
        

        修改代码并实现新方法

    7. 收尾工作

      resolver.gopackageimport之间添加如下行:

      //go:generate go run github.com/99designs/gqlgen generate
      

      之后运行go generate命令就会自动执行代码生成指令

    GraphQL基础语法

    参考文档

    模式定义语言:Schema Definition Language.

    语法说明

    • Exampel One
    type Character {
        name: String!
        appearsIn: [Episode!]!
    }
    
    1. type Character 指示Character为一个包含一个或多个字段的对象(此处包含两个字段,nameappearsIn

    2. String! 说明 name 字段为内建类型string(string),且不为null(!)。

    3. [Episode!]!说明appearsIn是一个Episode(其中Episode不能为空)的数组,且不能为空(!)。

    • Example Two
    type Starship {
        id: ID!
        name: String!
        length(unit: LengthUnit = METER): Float
    }
    

    对象的每一个字段都可以有零个或多个参数。参数可以是必须的也可以是可选的。当以参数为可选的时,我们可以为其设定默认值。如unit参数如果没有传入,则会被默认为METER

    • Example Three
    type Query {
      hero(episode: Episode): Character
      droid(id: ID!): Droid
    }
    

    GraphQL中有两个特殊的类型(type),QueryMutation。一个GraphQL的服务有一个Query类型和至多一个mutation类型。这两个类型与普通type的定义规则一致,但是他们是用来定义GraphQL查询入口的。

    1. type Query指明服务需要一个包含herodroid字段的Query入口。

    2. Mutation的工作方式和你定义Mutation字段相似,可以作为查询的根Mutation使用。

    • 标量(Scalar types)

      1. 没有子字段

      2. GraphQL提供了几个内建标量

        1. Int:32为有符号整型
        2. Float:双精度浮点数
        3. String:utf8 字符序列
        4. Boolean:true或者false
        5. ID:代表唯一ID,通常用于重新获取对象或用于缓存。序列化方式和String一样。
      3. 自定义标量

    scalar Date
    

    之后可以根据需求设定它的序列化、反序列化、校验方式。

    • 枚举类型(Enumeration types)

      enum Episode {
        NEWHOPE
        EMPIRE
        JEDI
      }
      
    • 列表和非空(List and Non-Null)

      1. 非空可以用于声明字段值不为空。用法查看Example One
      2. 非空还可以用于查询参数非空的说明
      query DroidById($id: ID!) {
        droid(id: $id) {
          name
        }
      }
      
    • 接口(Interfac)

      一个接口类型实现了接口对象应该包含特定的字段、参数、返回类型。

    interface Character {
      id: ID!
      name: String!
      friends: [Character]
      appearsIn: [Episode]!
    }
    
    type Human implements Character {
      id: ID!
      name: String!
      friends: [Character]
      appearsIn: [Episode]!
      starships: [Starship]
      totalCredits: Int
    }
    
    type Droid implements Character {
      id: ID!
      name: String!
      friends: [Character]
      appearsIn: [Episode]!
      primaryFunction: String
    }
    
    • 联合类型(Union Type)

      类似与Interface,但是不需要有共同的字段。

    union SearchResult = Human | Droid | Starship
    
    • 输入类型(Input Type)
    input ReviewInput {
      stars: Int!
      commentary: String
    }
    

    用于向mutation中加入复杂对象>

    mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
      createReview(episode: $ep, review: $review) {
        stars
        commentary
      }
    }
    

\